Skip to content

Commit 46812cb

Browse files
authored
Merge pull request #53 from netfoundry/release-v5.11.3
run ci test in aws
2 parents 0b94994 + ed66110 commit 46812cb

File tree

8 files changed

+319
-104
lines changed

8 files changed

+319
-104
lines changed

.github/workflows/main.yml

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,33 +72,13 @@ jobs:
7272
register-python-argcomplete nfctl
7373
7474
- name: Run the NF CLI demo to test installed version
75+
id: test_demo
7576
shell: bash
7677
env:
7778
NETFOUNDRY_CLIENT_ID: ${{ secrets.NETFOUNDRY_CLIENT_ID }}
7879
NETFOUNDRY_PASSWORD: ${{ secrets.NETFOUNDRY_PASSWORD }}
7980
NETFOUNDRY_OAUTH_URL: ${{ secrets.NETFOUNDRY_OAUTH_URL }}
80-
run: |
81-
set -o xtrace
82-
set -o pipefail
83-
84-
nfctl config \
85-
general.network=$(nfctl demo --echo-name --prefix 'gh-${{ github.run_id }}') \
86-
general.yes=True \
87-
general.verbose=yes || true # FIXME: sometimes config command exits with an error
88-
nfctl demo \
89-
--size medium \
90-
--regions us-ashburn-1 us-phoenix-1 \
91-
--provider OCI
92-
nfctl \
93-
list services
94-
nfctl \
95-
get service name=echo% > /tmp/echo.yml
96-
nfctl \
97-
delete service name=echo%
98-
nfctl \
99-
create service --file /tmp/echo.yml
100-
nfctl \
101-
delete network
81+
run: ./scripts/test-demo.sh
10282

10383
- name: Publish Test Package
10484
uses: pypa/gh-action-pypi-publish@v1.13.0
@@ -162,3 +142,43 @@ jobs:
162142
platforms: linux/amd64,linux/arm64
163143
push: true
164144
tags: ${{ steps.compose_tags.outputs.container_tags }}
145+
146+
cleanup-delay:
147+
if: failure()
148+
needs: [build_pypi_and_docker]
149+
runs-on: ubuntu-latest
150+
steps:
151+
- name: Wait 30 minutes before cleanup
152+
run: |
153+
echo "Test demo failed to complete. Waiting 30 minutes before cleanup to allow investigation..."
154+
sleep 1800
155+
156+
cleanup-network:
157+
if: always() && needs.build_pypi_and_docker.result == 'failure'
158+
needs: [cleanup-delay]
159+
runs-on: ubuntu-latest
160+
steps:
161+
- uses: actions/checkout@v5
162+
163+
- name: Set up Python
164+
uses: actions/setup-python@v6
165+
with:
166+
python-version: '3.12'
167+
168+
- name: Install nfctl
169+
run: |
170+
python -m pip install --upgrade pip
171+
pip install .
172+
173+
- name: Delete test network
174+
env:
175+
NETFOUNDRY_CLIENT_ID: ${{ secrets.NETFOUNDRY_CLIENT_ID }}
176+
NETFOUNDRY_PASSWORD: ${{ secrets.NETFOUNDRY_PASSWORD }}
177+
NETFOUNDRY_OAUTH_URL: ${{ secrets.NETFOUNDRY_OAUTH_URL }}
178+
run: |
179+
# Use wildcard pattern to match network created by this run
180+
NETWORK_PATTERN="gh-${GITHUB_RUN_ID}-%"
181+
echo "Attempting to delete network matching: ${NETWORK_PATTERN}"
182+
183+
# Try to delete the network, ignore errors if it doesn't exist
184+
nfctl delete network "name=${NETWORK_PATTERN}" --yes || echo "Network may not exist or already deleted"

netfoundry/ctl.py

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import signal
1414
import jwt
1515
import tempfile
16+
from concurrent.futures import ThreadPoolExecutor, as_completed
1617
from json import dumps as json_dumps
1718
from json import load as json_load
1819
from json import loads as json_loads
@@ -22,6 +23,7 @@
2223
from subprocess import CalledProcessError
2324
from sys import exit as sysexit
2425
from sys import stderr, stdin, stdout
26+
from threading import Lock
2527
from xml.sax.xmlreader import InputSource
2628

2729
from jwt.exceptions import PyJWTError
@@ -44,7 +46,7 @@
4446

4547
# import milc cli
4648
from milc import cli, questions # noqa: E402
47-
# set milc options using new API
49+
# set milc options (requires milc >= 1.8.0)
4850
cli.milc_options(name='nfctl', author='NetFoundry', version=f'v{netfoundry_version}')
4951
# this creates the config subcommand
5052
from milc.subcommand import config # noqa: F401,E402
@@ -94,7 +96,7 @@ def __call__(self, parser, namespace, values, option_string=None):
9496
@cli.argument('-B', '--borders', default=True, action='store_boolean', help='print cell borders in text tables')
9597
@cli.argument('-H', '--headers', default=True, action='store_boolean', help='print column headers in text tables')
9698
@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)
98100
@cli.argument('--proxy', help=argparse.SUPPRESS)
99101
@cli.argument('--gateway', default="gateway", help=argparse.SUPPRESS)
100102
@cli.entrypoint('configure the CLI to manage a network')
@@ -961,40 +963,63 @@ def demo(cli):
961963
else:
962964
spinner.succeed(f"Found a hosted router in {region}")
963965

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]
982988
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)
9901015

9911016
if not len(hosted_edge_routers) > 0:
9921017
raise RuntimeError("unexpected problem with router placements, found zero hosted routers")
9931018

9941019
spinner.text = f"Waiting for {len(hosted_edge_routers)} hosted router(s) to provision"
9951020
with spinner:
9961021
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)
9981023
# ensure the router tunneler is available
9991024
# network.wait_for_entity_name_exists(entity_name=router['name'], entity_type='endpoint')
10001025
# router_tunneler = network.find_resources(type='endpoint', name=router['name'])[0]
@@ -1091,31 +1116,6 @@ def demo(cli):
10911116
services[svc]['properties'] = network.services(name=svc)[0]
10921117
spinner.succeed(sub("Finding", "Found", spinner.text))
10931118

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-
11191119
# create unless exists
11201120
app_wan_name = "Default Service Policy"
11211121
spinner.text = "Finding service policy"

netfoundry/network.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,14 @@ def patch_resource(self, patch: dict, type: str = None, id: str = None, wait: in
458458
headers=headers,
459459
json=pruned_patch
460460
)
461+
if after_response.status_code in range(400, 600):
462+
self.logger.debug(
463+
'%s\n%s %s\r\n%s\r\n\r\n%s',
464+
'-----------RESPONSE-----------',
465+
after_response.status_code, after_response.reason,
466+
'\r\n'.join('{}: {}'.format(k, v) for k, v in after_response.headers.items()),
467+
after_response.text
468+
)
461469
after_response.raise_for_status() # raise any gross errors immediately
462470
after_response_code = after_response.status_code
463471
if after_response_code in [STATUS_CODES.codes.OK, STATUS_CODES.codes.ACCEPTED]:

0 commit comments

Comments
 (0)