Skip to content

Commit 930616b

Browse files
committed
Merge branch 'release-v5.3.0'
2 parents 7995c15 + e454a33 commit 930616b

File tree

5 files changed

+208
-71
lines changed

5 files changed

+208
-71
lines changed

.github/workflows/main.yml

+32
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,38 @@ jobs:
5959
}
6060
echo ::set-output name=pypi_version::${PYPI_VERSION}
6161
62+
- name: Install NetFoundry from build tarball
63+
run: |
64+
pip3 install ./dist/netfoundry-*.tar.gz
65+
66+
- name: Compare installed version to PyPi version
67+
env:
68+
PYPI_VERSION: ${{ steps.read_version.outputs.pypi_version }}
69+
run: |
70+
INSTALLED_VERSION="$(python3 -m netfoundry.version)"
71+
if ! [[ ${PYPI_VERSION} == ${INSTALLED_VERSION#v} ]]; then
72+
echo "ERROR: PyPi and installed version do not match." >&2
73+
exit 1
74+
fi
75+
76+
- name: Run the demo script to smoke test installed version
77+
env:
78+
NETWORK_NAME: github_smoketest_run${{ github.run_id }}
79+
NETFOUNDRY_CLIENT_ID: ${{ secrets.NETFOUNDRY_CLIENT_ID }}
80+
NETFOUNDRY_PASSWORD: ${{ secrets.NETFOUNDRY_PASSWORD }}
81+
NETFOUNDRY_OAUTH_URL: ${{ secrets.NETFOUNDRY_OAUTH_URL }}
82+
run: |
83+
python3 -m netfoundry.demo \
84+
--network ${NETWORK_NAME} \
85+
--create-client \
86+
--create-private \
87+
--regions Americas \
88+
-- create
89+
python3 -m netfoundry.demo \
90+
--network ${NETWORK_NAME} \
91+
--yes \
92+
-- delete
93+
6294
- name: Append 'latest' tag if release published
6395
env:
6496
GITHUB_EVENT_ACTION: ${{ github.event.action }}

netfoundry/demo.py

+25-11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pathlib import Path
1313

1414
import netfoundry
15+
from netfoundry.utility import DC_PROVIDERS
1516

1617

1718
def main():
@@ -26,6 +27,13 @@ def main():
2627
help="the command to run",
2728
choices=["create","delete"]
2829
)
30+
parser.add_argument(
31+
"-y", "--yes",
32+
dest="yes",
33+
default=False,
34+
action="store_true",
35+
help="Skip interactive prompt to confirm destructive actions."
36+
)
2937
parser.add_argument(
3038
"-n", "--network",
3139
help="The name of your demo network"
@@ -43,7 +51,7 @@ def main():
4351
"-s", "--network-size",
4452
dest="size",
4553
default="small",
46-
help="Billable Network size to create",
54+
help="Network size to create",
4755
choices=["small","medium","large"]
4856
)
4957
parser.add_argument(
@@ -74,7 +82,7 @@ def main():
7482
default="AWS",
7583
required=False,
7684
help="cloud provider to host Edge Routers",
77-
choices=["AWS", "AZURE", "GCP", "OCP"]
85+
choices=DC_PROVIDERS
7886
)
7987
regions_group = parser.add_mutually_exclusive_group(required=False)
8088
regions_group.add_argument("--regions",
@@ -117,7 +125,7 @@ def main():
117125
if args.command == "create":
118126
network.wait_for_status("PROVISIONED",wait=999,progress=True)
119127
elif args.command == "delete":
120-
if query_yes_no("Permanently destroy Network \"{network_name}\" now?".format(network_name=network_name)):
128+
if args.yes or query_yes_no("Permanently destroy Network \"{network_name}\" now?".format(network_name=network_name)):
121129
network.delete_network(progress=True)
122130
else:
123131
print("Not deleting Network \"{network_name}\".".format(network_name=network_name))
@@ -126,22 +134,23 @@ def main():
126134
network_id = network_group.create_network(name=network_name,size=args.size,version=args.version)['id']
127135
network = netfoundry.Network(network_group, network_id=network_id)
128136
network.wait_for_status("PROVISIONED",wait=999,progress=True)
137+
elif args.command == "delete":
138+
print("Network \"{network_name}\" does not exist.".format(network_name=network_name))
139+
sys.exit()
129140
else:
130141
raise Exception("ERROR: failed to find a network named \"{:s}\"".format(network_name))
131142

132-
# existing hosted ERs
143+
# existing hosted routers
133144
hosted_edge_routers = network.edge_routers(only_hosted=True)
134145
# a list of places where Endpoints are dialing from
135146

136-
# a list of locations to place one hosted ER
147+
# a list of locations to place a hosted router
137148
fabric_placements = list()
138149
if args.location_codes:
139150
for location in args.location_codes:
140-
data_centers = [dc for dc in network.get_edge_router_data_centers(provider=args.provider,location_code=location)]
141-
data_center = data_centers[0]
142-
existing_count = len([er for er in hosted_edge_routers if er['dataCenterId'] == data_center['id']])
151+
existing_count = len([er for er in hosted_edge_routers if er['provider'] == args.provider and er['locationCode'] == args.location_code])
143152
if existing_count < 1:
144-
fabric_placements += [data_center]
153+
fabric_placements += [location]
145154
else:
146155
print("INFO: found a hosted Edge Router(s) in {location}".format(location=location))
147156

@@ -153,14 +162,17 @@ def main():
153162
"#"+location['locationCode'],
154163
"#"+location['provider']
155164
],
156-
data_center_id=location['id']
165+
provider=args.provider,
166+
location_code=location['locationCode']
157167
)
158168
hosted_edge_routers += [er]
159169
print("INFO: Placed Edge Router in {provider} ({location_name})".format(
160170
provider=location['provider'],
161171
location_name=location['locationName']
162172
))
163173
elif args.regions:
174+
if args.provider or args.location_codes:
175+
print("WARN: ignoring provider and location_codes because AWS georegion is specified.")
164176
geo_regions = args.regions
165177
for region in geo_regions:
166178
data_center_ids = [dc['id'] for dc in network.aws_geo_regions[region]]
@@ -180,9 +192,11 @@ def main():
180192
attributes=[
181193
"#defaultRouters",
182194
"#"+location['locationCode'],
195+
"#"+location['provider'],
183196
"#"+location['geoRegion']
184197
],
185-
data_center_id=location['id']
198+
provider="AWS",
199+
location_code=location['locationCode']
186200
)
187201
hosted_edge_routers += [er]
188202
print("INFO: Placed Edge Router in {major} ({location_name})".format(

netfoundry/network.py

+83-15
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from unicodedata import name # enforce a timeout; sleep
66
from uuid import UUID # validate UUIDv4 strings
77

8-
from .utility import (EXCLUDED_PATCH_PROPERTIES, HOST_PROPERTIES,
8+
from .utility import (DC_PROVIDERS, EXCLUDED_PATCH_PROPERTIES, HOST_PROPERTIES,
99
MAJOR_REGIONS, RESOURCES, STATUS_CODES, VALID_SEPARATORS,
1010
VALID_SERVICE_PROTOCOLS, docstring_parameters, eprint,
1111
http, plural, singular)
@@ -165,7 +165,7 @@ def validate_entity_roles(self, entities: list, type: str):
165165
an ERP.
166166
167167
:param list entities: required hashtag role attributes, existing entity @names, or existing entity UUIDs
168-
:param str type: required type of entity: one of {resource_entity_types}
168+
:param str type: required type of entity, choices: {resource_entity_types}
169169
"""
170170
valid_entities = list()
171171
for entity in entities:
@@ -197,8 +197,47 @@ def validate_entity_roles(self, entities: list, type: str):
197197
else: valid_entities.append('@'+entity_name) # is an existing endpoint's name resolved from UUID
198198
return(valid_entities)
199199

200-
def get_edge_router_data_centers(self,provider: str=None,location_code: str=None):
201-
"""list the data centers where an Edge Router may be created
200+
def get_data_center_by_id(self, id: str):
201+
"""Get data centers by UUIDv4.
202+
203+
:param id: required UUIDv4 of data center
204+
"""
205+
try:
206+
# data centers returns a list of dicts (data centers)
207+
headers = { "authorization": "Bearer " + self.session.token }
208+
209+
response = http.get(
210+
self.session.audience+'core/v2/data-centers/'+id,
211+
proxies=self.session.proxies,
212+
verify=self.session.verify,
213+
headers=headers
214+
)
215+
response_code = response.status_code
216+
except:
217+
raise
218+
219+
if response_code == STATUS_CODES.codes.OK: # HTTP 200
220+
try:
221+
data_center = json.loads(response.text)
222+
except ValueError as e:
223+
eprint('ERROR getting data center')
224+
raise(e)
225+
else:
226+
raise Exception(
227+
'ERROR: got unexpected HTTP code {:s} ({:d}) and response {:s}'.format(
228+
STATUS_CODES._codes[response_code][0].upper(),
229+
response_code,
230+
response.text
231+
)
232+
)
233+
return(data_center)
234+
235+
@docstring_parameters(providers=str(DC_PROVIDERS))
236+
def get_edge_router_data_centers(self, provider: str=None, location_code: str=None):
237+
"""Find data centers for hosting edge routers.
238+
239+
:param provider: optionally filter by data center provider, choices: {providers}
240+
:param location_code: provider-specific string identifying the data center location e.g. us-west-1
202241
"""
203242
try:
204243
# data centers returns a list of dicts (data centers)
@@ -208,10 +247,10 @@ def get_edge_router_data_centers(self,provider: str=None,location_code: str=None
208247
"hostType": "ER"
209248
}
210249
if provider is not None:
211-
if provider in ["AWS", "AZURE", "GCP", "OCP"]:
250+
if provider in DC_PROVIDERS:
212251
params['provider'] = provider
213252
else:
214-
raise Exception("ERROR: unexpected cloud provider {:s}".format(provider))
253+
raise Exception("ERROR: unexpected cloud provider {:s}. Need one of {:s}".format(provider,str(DC_PROVIDERS)))
215254
response = http.get(
216255
self.session.audience+'core/v2/data-centers',
217256
proxies=self.session.proxies,
@@ -377,6 +416,8 @@ def get_resources(self, type: str,name: str=None, accept: str=None, deleted: boo
377416
params['name'] = name
378417
if typeId is not None:
379418
params['typeId'] = typeId
419+
if deleted:
420+
params['status'] = "DELETED"
380421

381422
if not type in RESOURCES.keys():
382423
raise Exception("ERROR: unknown type \"{}\". Choices: {}".format(type, RESOURCES.keys()))
@@ -452,7 +493,9 @@ def get_resources(self, type: str,name: str=None, accept: str=None, deleted: boo
452493
)
453494
)
454495

455-
# omit deleted entities by default
496+
# prune entities with non null deletedAt unless return deleted is true
497+
# TODO: remove this because the API has been changed to stop returning
498+
# deleted entities unless query param status=DELETED
456499
if not deleted:
457500
all_entities = [entity for entity in all_entities if not entity['deletedAt']]
458501

@@ -828,14 +871,22 @@ def create_endpoint(self, name: str, attributes: list=[], session_identity: str=
828871
raise Exception("ERROR: timed out waiting for process status 'STARTED' or 'FINISHED'")
829872
return(started)
830873

874+
@docstring_parameters(providers=str(DC_PROVIDERS))
831875
def create_edge_router(self, name: str, attributes: list=[], link_listener: bool=False, data_center_id: str=None,
832-
tunneler_enabled: bool=False, wait: int=900, sleep: int=10, progress: bool=False):
876+
tunneler_enabled: bool=False, wait: int=900, sleep: int=10, progress: bool=False,
877+
provider: str=None, location_code: str=None):
833878
"""Create an Edge Router.
834879
880+
A router may be hosted by NetFoundry or the customer. If hosted by NF,
881+
then you must supply datacenter "provider" and "location_code". If
882+
neither are given then the router is customer hosted.
883+
835884
:param name: a meaningful, unique name
836885
:param attributes: a list of hashtag role attributes
837-
:param link_listener: true if router should listen for other routers' links on 80/tcp
838-
:param data_center_id: the UUIDv4 of a data center location that can host edge routers
886+
:param link_listener: true if router should listen for other routers' transit links on 80/tcp, always true if hosted by NetFoundry
887+
:param data_center_id: (DEPRECATED by provider, location_code) the UUIDv4 of a NetFoundry data center location that can host edge routers
888+
:param provider: datacenter provider, choices: {providers}
889+
:param location_code: provider-specific string identifying the datacenter location e.g. us-west-1
839890
:param tunneler_enabled: true if the built-in tunneler features should be enabled for hosting or interception or both
840891
:param wait: seconds to wait for async create to succeed
841892
"""
@@ -854,8 +905,25 @@ def create_edge_router(self, name: str, attributes: list=[], link_listener: bool
854905
"tunnelerEnabled": tunneler_enabled
855906
}
856907
if data_center_id:
857-
body['dataCenterId'] = data_center_id
908+
eprint('WARN: data_center_id is deprecated by provider, location_code. ')
909+
data_center = self.get_data_center_by_id(id=data_center_id)
910+
body['provider'] = data_center['provider']
911+
body['locationCode'] = data_center['locationCode']
858912
body['linkListener'] = True
913+
elif provider or location_code:
914+
if provider and location_code:
915+
data_centers = self.get_edge_router_data_centers(provider=provider, location_code=location_code)
916+
if len(data_centers) == 1:
917+
body['provider'] = provider
918+
body['locationCode'] = location_code
919+
body['linkListener'] = True
920+
else:
921+
raise Exception("ERROR: failed to find exactly one {provider} data center with location_code={location_code}".format(
922+
provider=provider,
923+
location_code=location_code))
924+
else:
925+
raise Exception("ERROR: need both provider and location_code to create a hosted router.")
926+
859927
response = http.post(
860928
self.session.audience+'core/v2/edge-routers',
861929
proxies=self.session.proxies,
@@ -990,7 +1058,7 @@ def create_service_simple(self, name: str, client_host_name: str, client_port: i
9901058
:param: client_port is required integer of the ports to intercept
9911059
:param: server_host_name is optional string that is a hostname (DNS) or IPv4. If omitted service is assumed to be SDK-hosted (not Tunneler or Router-hosted).
9921060
:param: server_port is optional integer of the server port. If omitted the client port is used unless SDK-hosted.
993-
:param: server_protocol is optional string of the server protocol.
1061+
:param: server_protocol is optional string of the server protocol, choices: {valid_service_protocols}. Default is ["tcp"].
9941062
:param: attributes is optional list of strings of service roles to assign. Default is [].
9951063
:param: edge_router_attributes is optional list of strings of Router roles or Router names that can "see" this service. Default is ["#all"].
9961064
:param: egress_router_id is optional string of UUID or name of hosting Router. Selects Router-hosting strategy.
@@ -1901,15 +1969,15 @@ def get_network_by_id(self,network_id):
19011969
return(network)
19021970

19031971
def wait_for_property_defined(self, property_name: str, property_type: object=str, entity_type: str="network", wait: int=60, sleep: int=3, id: str=None, progress: bool=False):
1904-
"""continuously poll until expiry for the expected property to become defined with the any value of the expected type
1972+
"""Poll until expiry for the expected property to become defined with the any value of the expected type.
1973+
19051974
:param: property_name a top-level property to wait for e.g. `zitiId`
19061975
:param: property_type optional Python instance type to expect for the value of property_name
19071976
:param: id the UUID of the entity having a status if entity is not a network
19081977
:param: entity_type optional type of entity e.g. network (default), endpoint, service, edge-router
19091978
:param: wait optional SECONDS after which to raise an exception defaults to five minutes (300)
19101979
:param: sleep SECONDS polling interval
19111980
"""
1912-
19131981
# use the id of this instance's Network unless another one is specified
19141982
if entity_type == "network" and not id:
19151983
id = self.id
@@ -1983,7 +2051,7 @@ def wait_for_entity_name_exists(self, entity_name: str, entity_type: str, wait:
19832051
"""Continuously poll until expiry for the expected entity name to exist.
19842052
19852053
:param: entity_name
1986-
:param: entity_type is singular or plural form, any of {resource_entity_types}
2054+
:param: entity_type is singular or plural form, choices: {resource_entity_types}
19872055
:param: wait optional SECONDS after which to raise an exception defaults to five minutes (300)
19882056
:param: sleep SECONDS polling interval
19892057
:param: progress print a horizontal progress meter as dots, default false

0 commit comments

Comments
 (0)