11
11
import platform
12
12
import re
13
13
import signal
14
+ import jwt
14
15
import tempfile
16
+ from builtins import list as blist
15
17
from json import dumps as json_dumps
16
18
from json import load as json_load
17
19
from json import loads as json_loads
39
41
from .network import Network , Networks
40
42
from .network_group import NetworkGroup
41
43
from .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
43
45
44
46
set_metadata (version = f"v{ netfoundry_version } " , author = "NetFoundry" , name = "nfctl" ) # must precend import milc.cli
45
47
from milc import cli , questions # this uses metadata set above
@@ -485,6 +487,14 @@ def get(cli, echo: bool = True, spinner: object = None):
485
487
matches = organization .find_roles (** cli .args .query )
486
488
if len (matches ) == 1 :
487
489
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 ]
488
498
elif cli .args .resource_type == "network" :
489
499
if 'id' in query_keys :
490
500
if len (query_keys ) > 1 :
@@ -522,27 +532,15 @@ def get(cli, echo: bool = True, spinner: object = None):
522
532
else :
523
533
cli .log .error ("need --network=ACMENet" )
524
534
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 )
536
540
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 ]
546
544
547
545
if match :
548
546
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):
601
599
@cli .argument ('-k' , '--keys' , arg_only = True , action = StoreListKeys , help = "list of keys as a,b,c to print only selected keys (columns)" )
602
600
@cli .argument ('-m' , '--my-roles' , arg_only = True , action = 'store_true' , help = "filter roles by caller identity" )
603
601
@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 )
604
603
@cli .argument ('resource_type' , arg_only = True , help = 'type of resource' , metavar = "RESOURCE_TYPE" ,
605
604
choices = [choice for group in [[type , RESOURCES [type ].abbreviation ] for type in RESOURCES .keys ()] for choice in group ])
606
605
@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
+ """
609
611
if not spinner :
610
612
spinner = get_spinner (cli , "working" )
611
613
else :
612
614
cli .log .debug ("got spinner as function param" )
613
615
if RESOURCE_ABBREV .get (cli .args .resource_type ):
614
616
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 ):
616
618
cli .log .warn ("the --as=ACCEPT param is not applicable to resources outside the network domain" )
617
619
if cli .args .query and cli .args .query .get ('id' ):
618
620
cli .log .warn ("try 'get' command to get by id" )
@@ -627,6 +629,8 @@ def list(cli, spinner: object = None):
627
629
spinner .text = f"Finding { cli .args .resource_type } { 'by' if query_keys else '...' } { ', ' .join (query_keys )} "
628
630
else :
629
631
spinner .text = f"Finding all { cli .args .resource_type } "
632
+ if not echo :
633
+ spinner .enabled = False
630
634
with spinner :
631
635
organization , networks = use_organization (cli , spinner )
632
636
if cli .args .resource_type == "organizations" :
@@ -645,6 +649,8 @@ def list(cli, spinner: object = None):
645
649
matches = organization .find_roles (** cli .args .query )
646
650
else :
647
651
matches = organization .find_roles (** cli .args .query )
652
+ elif cli .args .resource_type == "regions" :
653
+ matches = networks .find_regions (** cli .args .query )
648
654
elif cli .args .resource_type == "networks" :
649
655
if cli .config .general .network_group :
650
656
network_group = use_network_group (
@@ -666,10 +672,7 @@ def list(cli, spinner: object = None):
666
672
else :
667
673
cli .log .error ("first configure a network: '--network=ACMENet'" )
668
674
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 )
673
676
674
677
if len (matches ) == 0 :
675
678
spinner .fail (f"Found no { cli .args .resource_type } by '{ ', ' .join (query_keys )} '" )
@@ -678,20 +681,26 @@ def list(cli, spinner: object = None):
678
681
cli .log .debug (f"found at least one { cli .args .resource_type } by '{ ', ' .join (query_keys )} '" )
679
682
680
683
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 )
681
696
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 )} " )
692
701
693
702
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 )} " )
695
704
filtered_matches = []
696
705
for match in matches :
697
706
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):
700
709
cli .log .debug ("not filtering output keys" )
701
710
filtered_matches = matches
702
711
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
+
703
716
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
+
704
739
if cli .config .general .headers :
705
740
table_headers = filtered_matches [0 ].keys ()
706
741
else :
@@ -898,7 +933,7 @@ def demo(cli):
898
933
name = network_name ,
899
934
size = cli .config .demo .size ,
900
935
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
+ )
902
937
network , network_group = use_network (
903
938
cli ,
904
939
organization = organization ,
@@ -913,8 +948,8 @@ def demo(cli):
913
948
# a list of locations to place a hosted router
914
949
fabric_placements = []
915
950
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 :
918
953
raise RuntimeError (f"invalid region '{ region } '" )
919
954
else :
920
955
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):
1153
1188
raise NFAPINoCredentials ()
1154
1189
spinner .succeed (f"Logged in profile '{ cli .config .general .profile } '" )
1155
1190
cli .log .debug (f"logged-in organization label is { organization .label } ." )
1156
- networks = Networks (Organization = organization )
1191
+ networks = Networks (setup = organization )
1157
1192
return organization , networks
1158
1193
1159
1194
@@ -1181,7 +1216,7 @@ def use_network_group(cli, organization: object, group: str = None, spinner: obj
1181
1216
return network_group
1182
1217
1183
1218
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 ):
1185
1220
"""
1186
1221
Use a network.
1187
1222
@@ -1334,6 +1369,33 @@ def get_spinner(cli, text):
1334
1369
return inner_spinner
1335
1370
1336
1371
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
+
1337
1399
yaml_lexer = get_lexer_by_name ("yaml" , stripall = True )
1338
1400
json_lexer = get_lexer_by_name ("json" , stripall = True )
1339
1401
bash_lexer = get_lexer_by_name ("bash" , stripall = True )
0 commit comments