1313import sys
1414import tempfile
1515import time
16+ import logging
1617#from base64 import b64decode
1718from json import dumps as json_dumps
1819from json import loads as json_loads
20+ from re import sub
1921from subprocess import call
2022
2123#from cryptography.hazmat.primitives.serialization import Encoding, pkcs7
2224from jwt .exceptions import PyJWTError
2325from milc import set_metadata
26+ from packaging import version
2427#from requests import get as http_get
2528from tabulate import tabulate
2629from yaml import dump as yaml_dumps
3235from .network import Network
3336from .network_group import NetworkGroup
3437from .organization import Organization
35- from .utility import MUTABLE_NETWORK_RESOURCES , NETWORK_RESOURCES , RESOURCES , Utility , plural , singular
36- from packaging import version
38+ from .utility import ( MUTABLE_NETWORK_RESOURCES , NETWORK_RESOURCES , RESOURCES ,
39+ Utility , plural , singular )
3740
3841set_metadata (version = "v" + get_versions ()['version' ]) # must precend import milc.cli
3942import milc .subcommand .config
@@ -300,24 +303,22 @@ def create(cli):
300303 cli .log .error ("failed to parse input lines as an object (deserialized JSON or YAML)" )
301304 exit (1 )
302305
303- # if stdout is connected to a terminal then open the create/template input for editing and finish create on exit
304- if sys .stdout .isatty ():
305- create_object = edit_object_as_yaml (create_input_object )
306- else :
307- create_object = create_input_object
306+ create_object = edit_object_as_yaml (create_input_object )
308307
309308 organization = use_organization ()
310309
311310 if cli .args .resource_type == "network" :
312311 if cli .config .general .network_group :
313312 network_group = use_network_group (organization = organization )
314- elif len (organization .get_network_groups_by_organization ()) > 1 :
315- cli .log .error ("specify --network-group because there is more than one available to caller's identity" )
316- raise SystemExit ()
317313 else :
318- network_group_id = organization .get_network_groups_by_organization ()[0 ]['id' ]
319- network_group = use_network_group (organization = organization , id = network_group_id )
320- resource = network .create_resource (type = cli .args .resource_type , properties = create_object , wait = cli .config .create .wait )
314+ org_count = len (organization .get_network_groups_by_organization ())
315+ if org_count > 1 :
316+ cli .log .error ("specify --network-group because there is more than one available to caller's identity" )
317+ exit (org_count )
318+ else : # use the only available group
319+ network_group_id = organization .get_network_groups_by_organization ()[0 ]['id' ]
320+ network_group = use_network_group (organization = organization , group = network_group_id )
321+ network = network_group .create_network (** create_object )
321322 else :
322323 network , network_group = use_network (
323324 organization = organization ,
@@ -349,7 +350,8 @@ def edit(cli):
349350@cli .argument ('resource_type' , arg_only = True , help = 'type of resource' , choices = [singular (type ) for type in RESOURCES .keys ()])
350351@cli .subcommand ('get a single resource by query' )
351352def get (cli , echo : bool = True ):
352- """Get a single resource as a dictionary."""
353+ """Get a single resource as YAML or JSON"""
354+ cli .log .setLevel (logging .WARN ) # don't emit INFO messages to stdout because they will break deserialization
353355 organization = use_organization ()
354356 match = {}
355357 matches = []
@@ -457,8 +459,12 @@ def get(cli, echo: bool=True):
457459@cli .subcommand ('find resources as lists' )
458460def list (cli ):
459461 """Find resources as lists."""
460- if not sys .stdout .isatty ():
461- cli .log .warn ("nfctl does not have a stable CLI interface. Use with caution in scripts." )
462+ if cli .config .general .output == "text" :
463+ if not sys .stdout .isatty ():
464+ cli .log .warn ("nfctl does not have a stable CLI interface. Use with caution in scripts." )
465+ else : # output is YAML or JSON
466+ # don't emit INFO messages to stdout because they will break deserialization
467+ cli .log .setLevel (logging .WARN )
462468
463469 organization = use_organization ()
464470
@@ -558,16 +564,24 @@ def delete(cli):
558564 cli .log .warn ("ignoring name='%s' because this operation applies to the entire network that is already selected" , str (cli .args .query ))
559565 try :
560566 if cli .args .yes or questions .yesno ("confirm delete network '{name}'" .format (name = network .name ), default = False ):
561- spinner .text = ' deleting {net}' .format (net = network .name )
562- with spinner :
563- try :
567+ spinner .text = " deleting network ' {net}'" .format (net = network .name )
568+ try :
569+ with spinner :
564570 network .delete_network (progress = False , wait = cli .config .delete .wait )
565- except KeyboardInterrupt as e :
566- cli .log .debug ("wait cancelled by user" )
571+ except KeyboardInterrupt as e :
572+ cli .log .debug ("wait cancelled by user" )
573+ except Exception as e :
574+ cli .log .error ("unknown error in %s" , e )
575+ exit (1 )
576+ else :
577+ cli .log .info (sub ('deleting' , 'deleted' , spinner .text ))
567578 else :
568579 cli .echo ("not deleting network '{name}'." .format (name = network .name ))
569580 except KeyboardInterrupt as e :
570581 cli .log .debug ("input cancelled by user" )
582+ except Exception as e :
583+ cli .log .error ("unknown error in %s" , e )
584+ exit (1 )
571585 else :
572586 if not cli .args .query :
573587 cli .log .error ("need query to select a resource" )
@@ -587,15 +601,23 @@ def delete(cli):
587601 try :
588602 if cli .args .yes or questions .yesno ("confirm delete {type} '{name}'" .format (type = cli .args .resource_type , name = matches [0 ]['name' ]), default = False ):
589603 spinner .text = "deleting {type} '{name}'" .format (type = cli .args .resource_type , name = matches [0 ]['name' ])
590- with spinner :
591- try :
604+ try :
605+ with spinner :
592606 network .delete_resource (type = cli .args .resource_type , id = matches [0 ]['id' ])
593- except KeyboardInterrupt as e :
594- cli .log .debug ("wait cancelled by user" )
607+ except KeyboardInterrupt as e :
608+ cli .log .debug ("wait cancelled by user" )
609+ except Exception as e :
610+ cli .log .error ("unknown error in %s" , e )
611+ exit (1 )
612+ else :
613+ cli .log .info (sub ('deleting' , 'deleted' , spinner .text ))
595614 else :
596615 cli .echo ("not deleting {type} '{name}'" .format (type = cli .args .resource_type , name = matches [0 ]['name' ]))
597616 except KeyboardInterrupt as e :
598617 cli .log .debug ("input cancelled by user" )
618+ except Exception as e :
619+ cli .log .error ("unknown error in %s" , e )
620+ exit (1 )
599621
600622def use_organization (prompt : bool = True ):
601623 """Assume an identity in an organization."""
@@ -627,6 +649,9 @@ def use_organization(prompt: bool=True):
627649 except KeyboardInterrupt as e :
628650 cli .log .debug ("input cancelled by user" )
629651 exit (1 )
652+ except Exception as e :
653+ cli .log .error ("unknown error in %s" , e )
654+ exit (1 )
630655 try :
631656 organization = Organization (
632657 credentials = cli .config .general .credentials if cli .config .general .credentials else None ,
@@ -636,6 +661,10 @@ def use_organization(prompt: bool=True):
636661 proxy = cli .config .general .proxy
637662 )
638663 except PyJWTError as e :
664+ cli .log .error ("caught JWT error in %e" , e )
665+ exit (1 )
666+ except Exception as e :
667+ cli .log .error ("unknown error in %s" , e )
639668 exit (1 )
640669 else :
641670 cli .log .info ("not logged in" )
@@ -688,7 +717,7 @@ def use_network(organization: object, network: str=None, group: str=None, operat
688717 exit (1 )
689718
690719 spinner = cli .spinner (text = str (), spinner = 'dots12' , stream = sys .stderr )
691- if sys .stdout .isatty ():
720+ if sys .stdout .isatty () and cli . log . getEffectiveLevel () <= logging . INFO :
692721 spinner .enabled = True
693722 else :
694723 spinner .enabled = False
@@ -703,25 +732,42 @@ def use_network(organization: object, network: str=None, group: str=None, operat
703732 network .wait_for_statuses (["DELETING" ,"DELETED" ],wait = 999 ,progress = False )
704733 except KeyboardInterrupt as e :
705734 cli .log .debug ("wait cancelled by user" )
735+ except Exception as e :
736+ cli .log .error ("unknown error in %s" , e )
737+ exit (1 )
738+ else :
739+ cli .log .info ("network '{net}' deleted" .format (net = network_identifier ))
706740 elif operation in ['create' ,'read' ,'update' ]:
707- spinner .text = 'waiting for {net} to have status PROVISIONED' .format (net = network_identifier )
708- try :
709- with spinner :
710- network .wait_for_status ("PROVISIONED" ,wait = 999 ,progress = False )
711- except KeyboardInterrupt as e :
712- cli .log .debug ("wait cancelled by user" )
741+ if not network .status == 'PROVISIONED' :
742+ spinner .text = 'waiting for {net} to have status PROVISIONED' .format (net = network_identifier )
743+ try :
744+ with spinner :
745+ network .wait_for_status ("PROVISIONED" ,wait = 999 ,progress = False )
746+ except KeyboardInterrupt as e :
747+ cli .log .debug ("wait cancelled by user" )
748+ except Exception as e :
749+ cli .log .error ("unknown error in %s" , e )
750+ exit (1 )
751+ else :
752+ cli .log .info ("network '{net}' ready" .format (net = network_identifier ))
753+
713754 return network , network_group
714755
715756def edit_object_as_yaml (edit : object ):
716757 """Edit a resource object as YAML and return as object upon exit.
717758
718759 :param obj input: a deserialized (object) to edit and return as yaml
719760 """
720- if cli .args .yes :
761+ # unless --yes (config general.yes), if stdout is connected to a terminal
762+ # then open input for editing and send on exit
763+ if not sys .stdout .isatty () or cli .args .yes :
721764 return edit
765+ save_error = False
722766 EDITOR = os .environ .get ('NETFOUNDRY_EDITOR' ,os .environ .get ('EDITOR' ,'vim' ))
767+ instructions_bytes = "# just exit to confirm, or\n # abort by saving an empty file\n " .encode ()
768+ edit_bytes = yaml_dumps (edit , default_flow_style = False ).encode ()
723769 with tempfile .NamedTemporaryFile (suffix = ".yml" ) as tf :
724- tf .write (yaml_dumps ( edit , default_flow_style = False ) )
770+ tf .write (instructions_bytes + edit_bytes )
725771 tf .flush ()
726772 return_code = call (EDITOR .split ()+ [tf .name ])
727773
@@ -732,18 +778,20 @@ def edit_object_as_yaml(edit: object):
732778 edited_object = yaml_loads (edited )
733779 except parser .ParserError as e :
734780 cli .log .error ("invalid YAML or JSON: %s" , e )
735- with tempfile .NamedTemporaryFile (suffix = ".yml" ) as tf :
736- tf .write (edited )
737- cli .log .warn ("your buffer was saved in %s and you may edit and redirect to the same command as stdin or --file" , tf .name )
738- exit (1 )
781+ save_error = True
739782 except Exception as e :
740783 cli .log .error ("unknown error in %s" , e )
741- exit ( 1 )
784+ save_error = True
742785 else :
743786 return edited_object
744787 else :
745- cli .log .debug ("error editing temporary file" )
746- exit (1 )
788+ cli .log .error ("editor returned an error" )
789+ save_error = True
790+ if save_error :
791+ with tempfile .NamedTemporaryFile (suffix = ".yml" ) as tf :
792+ tf .write (edited .encode ())
793+ cli .log .warn ("your buffer was saved in %s and you may edit and redirect to the same command as stdin or --file" , tf .name )
794+
747795
748796if __name__ == '__main__' :
749797 cli ()
0 commit comments