|
13 | 13 | import signal |
14 | 14 | import jwt |
15 | 15 | import tempfile |
| 16 | +from concurrent.futures import ThreadPoolExecutor, as_completed |
16 | 17 | from json import dumps as json_dumps |
17 | 18 | from json import load as json_load |
18 | 19 | from json import loads as json_loads |
|
22 | 23 | from subprocess import CalledProcessError |
23 | 24 | from sys import exit as sysexit |
24 | 25 | from sys import stderr, stdin, stdout |
| 26 | +from threading import Lock |
25 | 27 | from xml.sax.xmlreader import InputSource |
26 | 28 |
|
27 | 29 | from jwt.exceptions import PyJWTError |
|
44 | 46 |
|
45 | 47 | # import milc cli |
46 | 48 | from milc import cli, questions # noqa: E402 |
47 | | -# set milc options using new API |
| 49 | +# set milc options (requires milc >= 1.8.0) |
48 | 50 | cli.milc_options(name='nfctl', author='NetFoundry', version=f'v{netfoundry_version}') |
49 | 51 | # this creates the config subcommand |
50 | 52 | from milc.subcommand import config # noqa: F401,E402 |
@@ -94,7 +96,7 @@ def __call__(self, parser, namespace, values, option_string=None): |
94 | 96 | @cli.argument('-B', '--borders', default=True, action='store_boolean', help='print cell borders in text tables') |
95 | 97 | @cli.argument('-H', '--headers', default=True, action='store_boolean', help='print column headers in text tables') |
96 | 98 | @cli.argument('-Y', '--yes', action='store_true', arg_only=True, help='answer yes to potentially-destructive operations') |
97 | | -@cli.argument('-W', '--wait', help='seconds to wait for long-running processes to finish', default=900) |
| 99 | +@cli.argument('-W', '--wait', type=int, help='seconds to wait for long-running processes to finish', default=900) |
98 | 100 | @cli.argument('--proxy', help=argparse.SUPPRESS) |
99 | 101 | @cli.argument('--gateway', default="gateway", help=argparse.SUPPRESS) |
100 | 102 | @cli.entrypoint('configure the CLI to manage a network') |
@@ -961,40 +963,63 @@ def demo(cli): |
961 | 963 | else: |
962 | 964 | spinner.succeed(f"Found a hosted router in {region}") |
963 | 965 |
|
964 | | - spinner.text = f"Creating {len(fabric_placements)} hosted router(s)" |
965 | | - with spinner: |
966 | | - for region in fabric_placements: |
967 | | - er_name = f"Hosted Router {region} [{cli.config.demo.provider}]" |
968 | | - if not network.edge_router_exists(er_name): |
969 | | - er = network.create_edge_router( |
970 | | - name=er_name, |
971 | | - attributes=[ |
972 | | - "#hosted_routers", |
973 | | - "#demo_exits", |
974 | | - f"#{cli.config.demo.provider}", |
975 | | - ], |
976 | | - provider=cli.config.demo.provider, |
977 | | - location_code=region, |
978 | | - tunneler_enabled=False, # workaround for MOP-18098 (missing tunneler binding in ziti-router config) |
979 | | - ) |
980 | | - hosted_edge_routers.extend([er]) |
981 | | - spinner.succeed(f"Created {cli.config.demo.provider} router in {region}") |
| 966 | + # Helper function to create or validate a single router (runs in parallel) |
| 967 | + def create_or_validate_router(region): |
| 968 | + """Create or validate router for a region. Returns (region, router_dict, message).""" |
| 969 | + er_name = f"Hosted Router {region} [{cli.config.demo.provider}]" |
| 970 | + if not network.edge_router_exists(er_name): |
| 971 | + er = network.create_edge_router( |
| 972 | + name=er_name, |
| 973 | + attributes=[ |
| 974 | + "#hosted_routers", |
| 975 | + "#demo_exits", |
| 976 | + f"#{cli.config.demo.provider}", |
| 977 | + ], |
| 978 | + provider=cli.config.demo.provider, |
| 979 | + location_code=region, |
| 980 | + tunneler_enabled=False, # workaround for MOP-18098 (missing tunneler binding in ziti-router config) |
| 981 | + ) |
| 982 | + message = f"Created {cli.config.demo.provider} router in {region}" |
| 983 | + return (region, er, message) |
| 984 | + else: |
| 985 | + er_matches = network.edge_routers(name=er_name, only_hosted=True) |
| 986 | + if len(er_matches) == 1: |
| 987 | + er = er_matches[0] |
982 | 988 | else: |
983 | | - er_matches = network.edge_routers(name=er_name, only_hosted=True) |
984 | | - if len(er_matches) == 1: |
985 | | - er = er_matches[0] |
986 | | - else: |
987 | | - raise RuntimeError(f"unexpectedly found more than one matching router for name '{er_name}'") |
988 | | - if er['status'] in RESOURCES["edge-routers"].status_symbols["error"] + RESOURCES["edge-routers"].status_symbols["deleting"] + RESOURCES["edge-routers"].status_symbols["deleted"]: |
989 | | - raise RuntimeError(f"hosted router '{er_name}' has unexpected status '{er['status']}'") |
| 989 | + raise RuntimeError(f"unexpectedly found more than one matching router for name '{er_name}'") |
| 990 | + if er['status'] in RESOURCES["edge-routers"].status_symbols["error"] + RESOURCES["edge-routers"].status_symbols["deleting"] + RESOURCES["edge-routers"].status_symbols["deleted"]: |
| 991 | + raise RuntimeError(f"hosted router '{er_name}' has unexpected status '{er['status']}'") |
| 992 | + return (region, er, None) # No message for existing routers |
| 993 | + |
| 994 | + # Parallelize router creation with thread-safe spinner updates |
| 995 | + spinner.text = f"Creating {len(fabric_placements)} hosted router(s)" |
| 996 | + spinner_lock = Lock() |
| 997 | + new_routers = [] |
| 998 | + |
| 999 | + with ThreadPoolExecutor(max_workers=min(len(fabric_placements), 5)) as executor: |
| 1000 | + # Submit all router creation tasks |
| 1001 | + future_to_region = {executor.submit(create_or_validate_router, region): region for region in fabric_placements} |
| 1002 | + |
| 1003 | + # Collect results as they complete |
| 1004 | + for future in as_completed(future_to_region): |
| 1005 | + region, er, message = future.result() |
| 1006 | + new_routers.append(er) |
| 1007 | + |
| 1008 | + # Thread-safe spinner update for newly created routers |
| 1009 | + if message: |
| 1010 | + with spinner_lock: |
| 1011 | + spinner.succeed(message) |
| 1012 | + |
| 1013 | + # Add all new routers to the list |
| 1014 | + hosted_edge_routers.extend(new_routers) |
990 | 1015 |
|
991 | 1016 | if not len(hosted_edge_routers) > 0: |
992 | 1017 | raise RuntimeError("unexpected problem with router placements, found zero hosted routers") |
993 | 1018 |
|
994 | 1019 | spinner.text = f"Waiting for {len(hosted_edge_routers)} hosted router(s) to provision" |
995 | 1020 | with spinner: |
996 | 1021 | for router in hosted_edge_routers: |
997 | | - network.wait_for_statuses(expected_statuses=RESOURCES["edge-routers"].status_symbols["complete"], id=router['id'], type="edge-router", wait=2222, progress=False) |
| 1022 | + network.wait_for_statuses(expected_statuses=RESOURCES["edge-routers"].status_symbols["complete"], id=router['id'], type="edge-router", wait=cli.config.general.wait, progress=False) |
998 | 1023 | # ensure the router tunneler is available |
999 | 1024 | # network.wait_for_entity_name_exists(entity_name=router['name'], entity_type='endpoint') |
1000 | 1025 | # router_tunneler = network.find_resources(type='endpoint', name=router['name'])[0] |
@@ -1091,31 +1116,6 @@ def demo(cli): |
1091 | 1116 | services[svc]['properties'] = network.services(name=svc)[0] |
1092 | 1117 | spinner.succeed(sub("Finding", "Found", spinner.text)) |
1093 | 1118 |
|
1094 | | - # create a customer-hosted ER unless exists |
1095 | | - customer_router_name = "Branch Exit Router" |
1096 | | - spinner.text = f"Finding customer router '{customer_router_name}'" |
1097 | | - with spinner: |
1098 | | - if not network.edge_router_exists(name=customer_router_name): |
1099 | | - spinner.text = sub("Finding", "Creating", spinner.text) |
1100 | | - customer_router = network.create_edge_router( |
1101 | | - name=customer_router_name, |
1102 | | - attributes=["#branch_exit_routers"], |
1103 | | - tunneler_enabled=True) |
1104 | | - else: |
1105 | | - customer_router = network.edge_routers(name=customer_router_name)[0] |
1106 | | - spinner.succeed(sub("Finding", "Found", spinner.text)) |
1107 | | - |
1108 | | - spinner.text = f"Waiting for customer router {customer_router_name} to be ready for registration" |
1109 | | - # wait for customer router to be PROVISIONED so that registration will be available |
1110 | | - with spinner: |
1111 | | - try: |
1112 | | - network.wait_for_statuses(expected_statuses=RESOURCES["edge-routers"].status_symbols["complete"], id=customer_router['id'], type="edge-router", wait=222, progress=False) |
1113 | | - customer_router_registration = network.rotate_edge_router_registration(id=customer_router['id']) |
1114 | | - except Exception as e: |
1115 | | - raise RuntimeError(f"error getting router registration, got {e}") |
1116 | | - else: |
1117 | | - spinner.succeed(f"Customer router ready to register with key '{customer_router_registration['registrationKey']}'") |
1118 | | - |
1119 | 1119 | # create unless exists |
1120 | 1120 | app_wan_name = "Default Service Policy" |
1121 | 1121 | spinner.text = "Finding service policy" |
|
0 commit comments