When writing programs or frameworks, big or small, it is common to create utility or helper files that contain functions to perform specific operations. This makes it easy to import and call these functions in your main program file. In essence, you will be creating a Python module in this section to use in your command line program.
The additional package being installed via pip is rich
.
The rich
package will help will help display things with color and style in a terminal.
pip install rich
In your VSCode terminal pane, open a new Python file to start creating your helper utility:
code-server -r ~/workspace/ndfclab/scripts/ndfc_utils.py
Import the Python libraries that are required for the functions that you will define in this Python file. You will recognize some of the library names as the packages you pip installed.
import json
import time
from rich.progress import track
from rich.console import Console
from rich.table import Table
from operator import itemgetter
The first functions you define will use your ndclient class object to perform a GET operation to get your staging fabric's inventory, VRFs, and Networks for use in other parts of your code. The arguments passed to this function will be the ndclient object that is stored in the variable called ndfc and also the name of your fabric in NDFC that is stored in the argument called fabric.
These first functions you can see you are leveraging the send
method from your NDClient class you defined in your
connector class object. Again, the method
will be used to send requests to NDFC API endpoints along with the type of method operation, e.g. GET or POST for example.
The last function will be used as a helper function that makes use of the initial get functions. This function will wait for the status of the fabric to be in a specific state before continuing on with the next operation. This is crucial for deterministically ensuring a VRF is successfully created before it's attached to a Network, for instance.
def get_fabric_inventory(ndfc, fabric):
endpoint = f'/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{fabric}/inventory/switchesByFabric'
resp = ndfc.send(endpoint=endpoint, method='GET')
return resp.json()
def get_vrfs(ndfc, fabric):
endpoint = f'/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric}/vrfs'
resp = ndfc.send(endpoint=endpoint, method='GET')
return resp.json()
def get_networks(ndfc, fabric):
endpoint = f'/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric}/networks'
resp = ndfc.send(endpoint=endpoint, method='GET')
return resp.json()
def wait_for_status(getter, key, status="DEPLOYED", retry=30):
while retry > 0:
fabric_data = getter()
if status == "DEPLOYED":
all_deployed = all(data[key] == status for data in fabric_data)
else:
all_deployed = any(data[key] == status for data in fabric_data)
if all_deployed:
break
time.sleep(2)
retry -= 1
if not all_deployed:
raise Exception(f"Resource not deployed successfully! Retry count exceeded.")
Create the initial opening of your create VRFs function. This function also takes your ndclient object that is called ndfc, the name of your fabric in the fabric variable, and your ndfc variables that are coming from your YAML data.
Make use of your previously created get_fabric_inventory
and get_vrfs
functions and store them in variables.
Next, you'll use the VRF data you got from NDFC to compare what VRFs already exist in NDFC vs what is in your intended data set.
You implement operations such as this for things like idempotency or simply put you want to ensure your code always performs the same operation.
Additionally, if something already exists in NDFC, it is more efficient to not have to build the payload and send it to NDFC.
Further, some APIs will throw an error if an object already exists and you try to recreate it using a POST operation; in reality this is where a PUT operation comes into the picture.
The method used here is expanded to demonstrate to you what type of logical operation needs to be done to get the difference in data sets.
The last line uses a more complex Python capability, list comprehension, which builds the list of VRFs missing from NDFC using one line of code.
def create_vrfs(ndfc, fabric, ndfc_vars):
fabric_switches = get_fabric_inventory(ndfc, fabric)
fabric_vrfs = get_vrfs(ndfc, fabric)
existing_vrfs = []
for fabric_vrf in fabric_vrfs:
for vrf in ndfc_vars['vrfs']:
if fabric_vrf['vrfName'] == vrf['vrf_name']:
existing_vrfs.append(vrf)
missing_vrfs = [diff for diff in ndfc_vars['vrfs'] if diff not in existing_vrfs]
Next, create the code block that will use the NDFC API endpoint for creating VRFs in NDFC. A for loop is then used to iterate over your YAML data with a key lookup of vrfs. The loop iterates over all the VRFs defined in your YAML data and builds the payload NDFC expects to receive for the API endpoint. Lastly, using your ndfc ndclient object, you will send the generated payload to NDFC as a POST request, much like you did in Postman.
if not missing_vrfs:
print(f"All VRFs are already created successfully!")
return
else:
endpoint = f'/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/v2/fabrics/{fabric}/vrfs'
for vrf in missing_vrfs:
data = {
"fabric": fabric,
"vrfName": vrf['vrf_name'],
"vrfTemplate": "Default_VRF_Universal",
"vrfExtensionTemplate": "Default_VRF_Extension_Universal",
"vrfId": vrf['vrf_id'],
"vrfTemplateConfig": {
"vrfName": vrf['vrf_name'],
"vrfSegmentId": vrf['vrf_id'],
"vrfDescription": vrf['vrf_name'],
"vrfVlanId": vrf['vlan_id'],
"vrfVlanName": vrf['vrf_name']
}
}
resp = ndfc.send(endpoint=endpoint, method='POST', data=data)
if resp.status_code == 200:
fabric_vrfs = get_vrfs(ndfc, fabric)
for vrf in missing_vrfs:
if any(fabric_vrf.get("vrfName") == vrf.get("vrf_name") for fabric_vrf in fabric_vrfs):
print(f"VRF {data['vrfName']} created successfully!")
else:
raise Exception(f"VRF {data['vrfName']} not created successfully!")
else:
raise Exception(f"VRF {data['vrfName']} not created successfully!\nAPI error code is {str(resp.status_code)}")
With the VRF(s) created in NDFC, you now need to attach the VRFs to leaf switches that NDFC is managing using the VRF attach API endpoint.
Again, you need to generate the payload NDFC expects for this API endpoint. You achieve this by first defining your data that is a list of
dictionaries. From your YAML data, you specified what leafs to attach to the VRF, thus the first for loop exists to iterate over your list of switches.
The second for loop iterates over the fabric-switches
variable that was set from your get_fabric_inventory
function.
A conditional if statement is used to match two things: 1) if the switch in NDFC has an identified role as a leaf,
2) if each of your YAML data switches name for attachment matches the name of the switches in NDFC. If this condition is true, then the LAN attachment payload is built and updated.
Lastly, using your ndfc ndclient object, you will send the generated payload to NDFC as a POST request to perform the LAN attchment to the VRF, again, much like you did in Postman.
endpoint = f'/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/v2/fabrics/{fabric}/vrfs/attachments'
for vrf in ndfc_vars['vrfs']:
data = [
{
"vrfName": vrf['vrf_name'],
"lanAttachList": []
}
]
for switch in vrf['attach']:
for fabric_switch in fabric_switches:
if fabric_switch['switchRole'] == 'leaf' and switch == fabric_switch['logicalName']:
lan_attach_data = {
"fabric": fabric,
"vrfName": vrf['vrf_name'],
"serialNumber": fabric_switch['serialNumber'],
"vlan": vrf['vlan_id'],
"deployment": True
}
data[0]['lanAttachList'].append(lan_attach_data)
resp = ndfc.send(endpoint=endpoint, method='POST', data=data)
if resp.status_code == 200:
wait_for_status(lambda: get_vrfs(ndfc, fabric), "vrfStatus", status="PENDING")
print(f"VRF {data[0]['vrfName']} attached successfully!")
else:
raise Exception(f"VRF {data[0]['vrfName']} was not attached successfully!\nAPI error code is {str(resp.status_code)}")
This last step in your create_vrfs
function code block leverages the deployment API endpoint to actually deploy the VRF(s) to the switches via a POST request.
endpoint = f'/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/v2/fabrics/{fabric}/vrfs/deployments'
for vrf in ndfc_vars['vrfs']:
data = {
"vrfNames": vrf['vrf_name']
}
resp = ndfc.send(endpoint=endpoint, method='POST', data=data)
if resp.status_code == 200:
wait_for_status(lambda: get_vrfs(ndfc, fabric), "vrfStatus")
print(f"VRF {data['vrfNames']} deployed successfully!")
else:
raise Exception(f"VRF {data['vrfNames']} was not deployed successfully!\nAPI error code is {str(resp.status_code)}")
The next function in your utility helper has steps and arguments almost identical to your previous function for creating VRFs.
Start your create_networks
function.
def create_networks(ndfc, fabric, ndfc_vars):
fabric_switches = get_fabric_inventory(ndfc, fabric)
fabric_networks = get_networks(ndfc, fabric)
existing_networks = []
for fabric_network in fabric_networks:
for network in ndfc_vars['networks']:
if fabric_network['networkName'] == network['net_name']:
existing_networks.append(network)
missing_networks = [diff for diff in ndfc_vars['networks'] if diff not in existing_networks]
Next, create the code block that will use the NDFC API endpoint for creating Networks in NDFC. Like the VRFs function, a loop is needed to iterate over your YAML data for networks to generate the payload to send to NDFC via a POST request using your ndfc ndclient object.
if not missing_networks:
print(f"All Networks are already created successfully!")
return
else:
endpoint = f'/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/v2/fabrics/{fabric}/networks'
for network in missing_networks:
data = {
"fabric": fabric,
"networkName": network['net_name'],
"networkId": network['net_id'],
"networkTemplate": "Default_Network_Universal",
"networkExtensionTemplate": "Default_Network_Extension_Universal",
"vrf": network['vrf_name'],
"tenantName": "Mytenant",
"networkTemplateConfig": {
"vlanId": network['vlan_id'],
"gatewayIpAddress": network['ipv4_gw'],
"vlanName": network['net_name']
}
}
resp = ndfc.send(endpoint=endpoint, method='POST', data=data)
if resp.status_code == 200:
fabric_networks = get_networks(ndfc, fabric)
for network in missing_networks:
if any(fabric_network.get("networkName") == network.get("net_name") for fabric_network in fabric_networks):
print(f"Network {data['networkName']} created successfully!")
else:
raise Exception(f"Network {data['networkName']} not created successfully!")
else:
raise Exception(f"Network {data['networkName']} not created successfully!\nAPI error code is {str(resp.status_code)}")
With the Networks created in NDFC, you now need to attach the Networks to leaf switches that NDFC is managing using the set interface and Network attach API endpoints. Again, you need to generate the payload NDFC expects for these API endpoints using your defined switch attachments for the networks in your YAML data.
for network in ndfc_vars['networks']:
interface_data = {
"policy": "int_access_host",
"interfaces": []
}
attach_data = [
{
"networkName": network['net_name'],
"lanAttachList": []
}
]
for attachment in network['attach']:
for fabric_switch in fabric_switches:
if fabric_switch['switchRole'] == 'leaf' and attachment['switch'] == fabric_switch['logicalName']:
interface_access_data = {
"serialNumber": fabric_switch['serialNumber'],
"ifName": attachment['port'],
"nvPairs": {
"INTF_NAME": attachment['port'],
"BPDUGUARD_ENABLED": "true",
"PORTTYPE_FAST_ENABLED": "true",
"MTU": "jumbo",
"SPEED": "Auto",
"ACCESS_VLAN": "",
"DESC": "",
"CONF": "",
"ADMIN_STATE": "true",
"ENABLE_NETFLOW": "false",
"NETFLOW_MONITOR": ""
}
}
interface_data['interfaces'].append(interface_access_data)
lan_attach_data = {
"fabric": fabric,
"networkName": network['net_name'],
"serialNumber": fabric_switch['serialNumber'],
"switchPorts": attachment['port'],
"detachSwitchPorts": "",
"deployment": True
}
attach_data[0]['lanAttachList'].append(lan_attach_data)
endpoint = f'/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/interface'
resp = ndfc.send(endpoint=endpoint, method='POST', data=interface_data)
endpoint = f'/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/v2/fabrics/{fabric}/networks/attachments'
resp = ndfc.send(endpoint=endpoint, method='POST', data=attach_data)
if resp.status_code == 200:
wait_for_status(lambda: get_networks(ndfc, fabric), "networkStatus", status="PENDING")
print(f"Network {attach_data[0]['networkName']} attached successfully!")
else:
raise Exception(f"Network {attach_data[0]['networkName']} was not attached successfully!\nAPI error code is {str(resp.status_code)}")
This last step in your create_networks
function code block leverages the deployment API endpoint to actually deploy the Network(s) to the switches via a POST request.
One note of difference here in comparison with VRFs, you must iterate your YAML data of networks list to perform the attachment.
for network in ndfc_vars['networks']:
endpoint = f"/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/v2/fabrics/{fabric}/networks/{network['net_name']}/deploy"
resp = ndfc.send(endpoint=endpoint, method='POST')
if resp.status_code == 200:
wait_for_status(lambda: get_networks(ndfc, fabric), "networkStatus")
print(f"Network {network['net_name']} deployed successfully!")
else:
raise Exception(f"Network {network['net_name']} was not deployed successfully!\nAPI error code is {str(resp.status_code)}")
With the helper functions defined to create VRFs and Networks, you need to now create a couple of getter functions for getting this information from NDFC using your command line tool. This queried data will be parsed, formatted and presented in a tabular format in your terminal window at runtime.
Define a new function to use a GET operation to get NDFC's VRFs for a specific fabric. The function takes your ndclient object for connecting and sending the request, and then your specified fabric
name as parameters. You also initialize an empty list called vrf_data
for building your custom data of what you want out of the API response.
def tabulate_vrfs(ndfc, fabric):
vrf_data = []
resp_vrfs = get_vrfs(ndfc, fabric)
Using a for loop with rich
, you need to loop through the returned VRF data from NDFC. With rich
we can track this process and time. For this lab, this will be pretty quick as
there are not a lot of VRFs, thus a simple time (sleep) delay is added at the end of the loop to demonstrate this behavior and in a real environment, you can remove this induced delay.
Within the loop, you will populate your vrf_data
list by appending a table row list that is created from variable values from NDFC's API response. For example, the vrf_name
variable is set from the API response by using the keyname from the API, vrfName
.
for vrf in track(resp_vrfs, description="Processing VRFs"):
vrf_name = vrf['vrfName']
vni_id = vrf['vrfId']
vlan_id = json.loads(vrf['vrfTemplateConfig'])['vrfVlanId']
vrf_status = vrf['vrfStatus']
table_row = [vrf_name, str(vni_id), str(vlan_id), vrf_status]
vrf_data.append(table_row)
time.sleep(0.5) # This is to slow down the loop so you can see Rich in action
Lastly, using rich
's tabulating capabilities, build a fancy table. First you need to define the table column names. Next, there is a small if block that checks the deployment state;
if the status is deployed then the status will be presented to the user in green and if anything other, then presented to the user in red.
Then, Python's sorted
and operator itemgetter
is used to get the VNI entry of your data set and sort the table based on VNI number in descending order.
Finally, the data from your vrf_data
list that you built using the loop above is iterated over in another for loop and added to the table.
rich
's Console object is then used to print this to the terminal window.
table = Table(title="NDFC VRFs")
table.add_column("VRF", style="cyan", no_wrap=True)
table.add_column("VNI", style="magenta")
table.add_column("VLAN", style="yellow")
table.add_column("Status", style="green")
vrf_data = sorted(vrf_data, key=itemgetter(1))
for row in vrf_data:
table.add_row(row[0],row[1],row[2],row[3])
console = Console()
console.print(table)
Define a new function to use a GET operation to get NDFC's Networks for a specific fabric. The function takes your ndclient object for connecting and sending the request, and then your specified fabric
name as parameters. You also initialize an empty list called network_data
for building your custom data of what you want out of the API response.
def tabulate_networks(ndfc, fabric):
network_data = []
resp_networks = get_networks(ndfc, fabric)
Using a for loop with rich
, you need to loop through the returned Network data from NDFC. With rich
you can track this process and time just like for VRFs.
Within the loop, you will populate your network_data
list by appending a table row list that is created from variable values from NDFC's API response.
For example, the net_name
variable is set from the API response by using the keyname from the API, networkName
.
for network in track(resp_networks, description="Processing networks"):
net_name = network['networkName']
vni_id = network['networkId']
vlan_id = json.loads(network['networkTemplateConfig'])['vlanId']
gw_ip = json.loads(network['networkTemplateConfig'])['gatewayIpAddress']
vrf_name = json.loads(network['networkTemplateConfig'])['vrfName']
net_status = network['networkStatus']
table_row = [net_name, str(vni_id), str(vlan_id), gw_ip, vrf_name, net_status]
network_data.append(table_row)
time.sleep(0.5) # This is to slow down the loop so you can see Rich in action
rich
is again leveraged to build a table just like was done for your Network's data set and using the Console object to print this to the terminal window.
table = Table(title="NDFC Networks")
table.add_column("Network", style="cyan", no_wrap=True)
table.add_column("VNI", style="magenta")
table.add_column("VLAN", style="yellow")
table.add_column("Anycast Gw", style="purple")
table.add_column("VRF", style="red")
table.add_column("Status", style="green")
network_data = sorted(network_data, key=itemgetter(1))
for row in network_data:
table.add_row(row[0],row[1],row[2],row[3],row[4],row[5])
console = Console()
console.print(table)
Be sure to save your file! Not saving will result in your code not executing.
Continue to the next section to see all your Python files and code come together by executing your command line program.