1111import platform
1212import re
1313import signal
14+ import jwt
1415import tempfile
16+ from builtins import list as blist
1517from json import dumps as json_dumps
1618from json import load as json_load
1719from json import loads as json_loads
3941from .network import Network , Networks
4042from .network_group import NetworkGroup
4143from .organization import Organization
42- from .utility import DC_PROVIDERS , EMBED_NET_RESOURCES , MUTABLE_NET_RESOURCES , MUTABLE_RESOURCE_ABBREV , RESOURCE_ABBREV , RESOURCES , is_jwt , normalize_caseless , plural , singular
44+ from .utility import DC_PROVIDERS , EMBED_NET_RESOURCES , IDENTITY_ID_PROPERTIES , MUTABLE_NET_RESOURCES , MUTABLE_RESOURCE_ABBREV , RESOURCE_ABBREV , RESOURCES , any_in , get_generic_resource_by_type_and_id , normalize_caseless , plural , propid2type , singular
4345
4446set_metadata (version = f"v{ netfoundry_version } " , author = "NetFoundry" , name = "nfctl" ) # must precend import milc.cli
4547from milc import cli , questions # this uses metadata set above
@@ -485,6 +487,14 @@ def get(cli, echo: bool = True, spinner: object = None):
485487 matches = organization .find_roles (** cli .args .query )
486488 if len (matches ) == 1 :
487489 match = matches [0 ]
490+ elif cli .args .resource_type == "region" :
491+ if 'id' in query_keys :
492+ cli .log .error ("regions do not have an ID property, try provider and location_code params" )
493+ sysexit (1 )
494+ else :
495+ matches = networks .find_regions (** cli .args .query )
496+ if len (matches ) == 1 :
497+ match = matches [0 ]
488498 elif cli .args .resource_type == "network" :
489499 if 'id' in query_keys :
490500 if len (query_keys ) > 1 :
@@ -522,27 +532,15 @@ def get(cli, echo: bool = True, spinner: object = None):
522532 else :
523533 cli .log .error ("need --network=ACMENet" )
524534 sysexit (1 )
525- if cli .args .resource_type == "data-center" :
526- if 'id' in query_keys :
527- cli .log .warn ("data centers fetched by ID may not support this network's product version, try provider or locationCode params for safety" )
528- if len (query_keys ) > 1 :
529- query_keys .remove ('id' )
530- cli .log .warn (f"using 'id' only, ignoring params: '{ ', ' .join (query_keys )} '" )
531- match = network .get_data_center_by_id (id = cli .args .query ['id' ])
532- else :
533- matches = network .find_edge_router_data_centers (** cli .args .query )
534- if len (matches ) == 1 :
535- match = network .get_data_center_by_id (id = matches [0 ]['id' ])
535+ if 'id' in query_keys :
536+ if len (query_keys ) > 1 :
537+ query_keys .remove ('id' )
538+ cli .log .warn (f"using 'id' only, ignoring params: '{ ', ' .join (query_keys )} '" )
539+ match = network .get_resource_by_id (type = cli .args .resource_type , id = cli .args .query ['id' ], accept = cli .args .accept )
536540 else :
537- if 'id' in query_keys :
538- if len (query_keys ) > 1 :
539- query_keys .remove ('id' )
540- cli .log .warn (f"using 'id' only, ignoring params: '{ ', ' .join (query_keys )} '" )
541- match = network .get_resource_by_id (type = cli .args .resource_type , id = cli .args .query ['id' ], accept = cli .args .accept )
542- else :
543- matches = network .find_resources (type = cli .args .resource_type , accept = cli .args .accept , params = cli .args .query )
544- if len (matches ) == 1 :
545- match = matches [0 ]
541+ matches = network .find_resources (type = cli .args .resource_type , accept = cli .args .accept , params = cli .args .query )
542+ if len (matches ) == 1 :
543+ match = matches [0 ]
546544
547545 if match :
548546 cli .log .debug (f"found exactly one { cli .args .resource_type } by '{ ', ' .join (query_keys )} '" )
@@ -601,18 +599,22 @@ def get(cli, echo: bool = True, spinner: object = None):
601599@cli .argument ('-k' , '--keys' , arg_only = True , action = StoreListKeys , help = "list of keys as a,b,c to print only selected keys (columns)" )
602600@cli .argument ('-m' , '--my-roles' , arg_only = True , action = 'store_true' , help = "filter roles by caller identity" )
603601@cli .argument ('-a' , '--as' , dest = 'accept' , arg_only = True , choices = ['create' ], help = "request the as=create alternative form of the resources" )
602+ @cli .argument ('-n' , '--names' , default = False , action = 'store_boolean' , help = argparse .SUPPRESS )
604603@cli .argument ('resource_type' , arg_only = True , help = 'type of resource' , metavar = "RESOURCE_TYPE" ,
605604 choices = [choice for group in [[type , RESOURCES [type ].abbreviation ] for type in RESOURCES .keys ()] for choice in group ])
606605@cli .subcommand (description = 'find a collection of resources by type and query' )
607- def list (cli , spinner : object = None ):
608- """Find resources as lists."""
606+ def list (cli , echo : bool = True , spinner : object = None ):
607+ """Find resources as lists.
608+
609+ :param echo: False allows the caller to capture the return instead of printing the match
610+ """
609611 if not spinner :
610612 spinner = get_spinner (cli , "working" )
611613 else :
612614 cli .log .debug ("got spinner as function param" )
613615 if RESOURCE_ABBREV .get (cli .args .resource_type ):
614616 cli .args .resource_type = RESOURCE_ABBREV [cli .args .resource_type ].name
615- if cli .args .accept and not MUTABLE_NET_RESOURCES .get (cli .args .resource_type ): # mutable excludes data-centers
617+ if cli .args .accept and not MUTABLE_NET_RESOURCES .get (cli .args .resource_type ):
616618 cli .log .warn ("the --as=ACCEPT param is not applicable to resources outside the network domain" )
617619 if cli .args .query and cli .args .query .get ('id' ):
618620 cli .log .warn ("try 'get' command to get by id" )
@@ -627,6 +629,8 @@ def list(cli, spinner: object = None):
627629 spinner .text = f"Finding { cli .args .resource_type } { 'by' if query_keys else '...' } { ', ' .join (query_keys )} "
628630 else :
629631 spinner .text = f"Finding all { cli .args .resource_type } "
632+ if not echo :
633+ spinner .enabled = False
630634 with spinner :
631635 organization , networks = use_organization (cli , spinner )
632636 if cli .args .resource_type == "organizations" :
@@ -645,6 +649,8 @@ def list(cli, spinner: object = None):
645649 matches = organization .find_roles (** cli .args .query )
646650 else :
647651 matches = organization .find_roles (** cli .args .query )
652+ elif cli .args .resource_type == "regions" :
653+ matches = networks .find_regions (** cli .args .query )
648654 elif cli .args .resource_type == "networks" :
649655 if cli .config .general .network_group :
650656 network_group = use_network_group (
@@ -666,10 +672,7 @@ def list(cli, spinner: object = None):
666672 else :
667673 cli .log .error ("first configure a network: '--network=ACMENet'" )
668674 sysexit (1 )
669- if cli .args .resource_type == "data-centers" :
670- matches = network .find_edge_router_data_centers (** cli .args .query )
671- else :
672- matches = network .find_resources (type = cli .args .resource_type , accept = cli .args .accept , params = cli .args .query )
675+ matches = network .find_resources (type = cli .args .resource_type , accept = cli .args .accept , params = cli .args .query )
673676
674677 if len (matches ) == 0 :
675678 spinner .fail (f"Found no { cli .args .resource_type } by '{ ', ' .join (query_keys )} '" )
@@ -678,20 +681,26 @@ def list(cli, spinner: object = None):
678681 cli .log .debug (f"found at least one { cli .args .resource_type } by '{ ', ' .join (query_keys )} '" )
679682
680683 valid_keys = set ()
684+ for match in matches :
685+ # cli.log.debug(match)
686+ valid_keys = valid_keys .union (match .keys ())
687+
688+ # intersection of the set of valid, observed keys in the first match
689+ default_keys = ['name' , 'label' , 'organizationShortName' , 'type' , 'description' ,
690+ 'edgeRouterAttributes' , 'serviceAttributes' , 'endpointAttributes' ,
691+ 'status' , 'zitiId' , 'provider' , 'locationCode' , 'ipAddress' , 'networkVersion' ,
692+ 'active' , 'default' , 'region' , 'size' , 'attributes' , 'email' , 'productVersion' ,
693+ 'address' , 'binding' , 'component' ]
694+ if cli .config .list .names : # include identity IDs if --names
695+ default_keys .extend (IDENTITY_ID_PROPERTIES )
681696 if cli .args .keys :
682- # intersection of the set of valid, observed keys in the first match
683- # and the set of configured, desired keys
684- valid_keys = set (matches [0 ].keys ()) & set (cli .args .keys )
685- elif cli .args .output == "text" :
686- default_columns = ['name' , 'label' , 'organizationShortName' , 'type' , 'description' ,
687- 'edgeRouterAttributes' , 'serviceAttributes' , 'endpointAttributes' ,
688- 'status' , 'zitiId' , 'provider' , 'locationCode' , 'ipAddress' , 'networkVersion' ,
689- 'active' , 'default' , 'region' , 'size' , 'attributes' , 'email' , 'productVersion' ,
690- 'address' , 'binding' , 'component' ]
691- valid_keys = set (matches [0 ].keys ()) & set (default_columns )
697+ valid_keys = valid_keys .intersection (cli .args .keys )
698+ else :
699+ valid_keys = valid_keys .intersection (default_keys )
700+ cli .log .debug (f"filtering matches for valid keys: { str (valid_keys )} " )
692701
693702 if valid_keys :
694- cli .log .debug (f"valid keys: { str (valid_keys )} " )
703+ cli .log .debug (f"filtering matches for valid keys: { str (valid_keys )} " )
695704 filtered_matches = []
696705 for match in matches :
697706 filtered_match = {key : match [key ] for key in match .keys () if key in valid_keys }
@@ -700,7 +709,33 @@ def list(cli, spinner: object = None):
700709 cli .log .debug ("not filtering output keys" )
701710 filtered_matches = matches
702711
712+ # if echo=False then return the object and parent objects instead of printing with an output format
713+ if not echo :
714+ return filtered_matches , organization
715+
703716 if cli .args .output == "text" :
717+ if cli .config .list .names :
718+ # map any property names that look like a resource ID to the appropriate resource type so we can look up the name later
719+ type_by_prop = dict ()
720+ for key in valid_keys :
721+ if key not in ['zitiId' ]:
722+ if key in IDENTITY_ID_PROPERTIES :
723+ type_by_prop [key ] = 'identities'
724+ elif key .endswith ('Id' ):
725+ type_by_prop [key ] = propid2type (key )
726+
727+ for match in filtered_matches : # for each match
728+ if any_in (type_by_prop .keys (), match .keys ()): # if at least one property points to a resolvable ID (fast)
729+ for k , v in match .items (): # for each key in match (slow)
730+ if type_by_prop .get (k ): # if this is the property that points to a resolvable ID
731+ if v is None :
732+ cli .log .debug (f"unexpected value for { k } = { v } " )
733+ continue
734+ # get the resource with the name we're after
735+ resource , status = get_generic_resource_by_type_and_id (setup = organization , resource_type = type_by_prop [k ], resource_id = v )
736+ if resource .get ('name' ): # if the name property isn't empty
737+ match [k ] = f"{ resource ['name' ]} " # wedge the name into the ID column
738+
704739 if cli .config .general .headers :
705740 table_headers = filtered_matches [0 ].keys ()
706741 else :
@@ -898,7 +933,7 @@ def demo(cli):
898933 name = network_name ,
899934 size = cli .config .demo .size ,
900935 version = cli .config .demo .product_version ,
901- wait = 0 ) # FIXME: don't use wait > 0 until process-executions beta is launched, until then poll for status
936+ )
902937 network , network_group = use_network (
903938 cli ,
904939 organization = organization ,
@@ -913,8 +948,8 @@ def demo(cli):
913948 # a list of locations to place a hosted router
914949 fabric_placements = []
915950 for region in cli .config .demo .regions :
916- dc_matches = network . find_edge_router_data_centers ( provider = cli .config .demo .provider , location_code = region )
917- if not len (dc_matches ) == 1 :
951+ region_matches = networks . find_regions ( providers = [ cli .config .demo .provider ] , location_code = region )
952+ if not len (region_matches ) == 1 :
918953 raise RuntimeError (f"invalid region '{ region } '" )
919954 else :
920955 existing_count = len ([er for er in hosted_edge_routers if er ['provider' ] == cli .config .demo .provider and er ['region' ] == region ])
@@ -1153,7 +1188,7 @@ def use_organization(cli, spinner: object = None, prompt: bool = True):
11531188 raise NFAPINoCredentials ()
11541189 spinner .succeed (f"Logged in profile '{ cli .config .general .profile } '" )
11551190 cli .log .debug (f"logged-in organization label is { organization .label } ." )
1156- networks = Networks (Organization = organization )
1191+ networks = Networks (setup = organization )
11571192 return organization , networks
11581193
11591194
@@ -1181,7 +1216,7 @@ def use_network_group(cli, organization: object, group: str = None, spinner: obj
11811216 return network_group
11821217
11831218
1184- def use_network (cli , organization : object , network_name : str = None , group : str = None , spinner : object = None ):
1219+ def use_network (cli , organization : Organization , network_name : str = None , group : str = None , spinner : object = None ):
11851220 """
11861221 Use a network.
11871222
@@ -1334,6 +1369,33 @@ def get_spinner(cli, text):
13341369 return inner_spinner
13351370
13361371
1372+ def jwt_decode (token ):
1373+ # TODO: figure out how to stop doing this because the token is for the
1374+ # API, not this app, and so may change algorithm unexpectedly or stop
1375+ # being a JWT altogether, currently needed to build the URL for HTTP
1376+ # requests, might need to start using env config
1377+ """Parse the token and return claimset."""
1378+ try :
1379+ claim = jwt .decode (jwt = token , algorithms = ["RS256" ], options = {"verify_signature" : False })
1380+ except jwt .exceptions .PyJWTError as e :
1381+ raise jwt .exceptions .PyJWTError (f"failed to parse bearer token as JWT, caught { e } " )
1382+ except Exception as e :
1383+ raise RuntimeError (f"unexpected error parsing JWT, caught { e } " )
1384+ return claim
1385+
1386+
1387+ def is_jwt (token ):
1388+ """If is a JWT then True."""
1389+ try :
1390+ jwt_decode (token )
1391+ except jwt .exceptions .PyJWTError :
1392+ return False
1393+ except Exception as e :
1394+ raise RuntimeError (f"unexpected error parsing JWT, caught { e } " )
1395+ else :
1396+ return True
1397+
1398+
13371399yaml_lexer = get_lexer_by_name ("yaml" , stripall = True )
13381400json_lexer = get_lexer_by_name ("json" , stripall = True )
13391401bash_lexer = get_lexer_by_name ("bash" , stripall = True )
0 commit comments