diff --git a/LICENSE.md b/LICENSE.md index 254a369..185140a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ CISCO SAMPLE CODE LICENSE Version 1.1 - Copyright (c) 2020 Cisco and/or its affiliates + Copyright (c) 2024 Cisco and/or its affiliates These terms govern this Cisco Systems, Inc. ("Cisco"), example or demo source code and its associated documentation (together, the "Sample @@ -28,7 +28,7 @@ CISCO SAMPLE CODE LICENSE and services are licensed under their own separate terms and you shall not use the Sample Code in any way that violates or is inconsistent with those terms (for more information, please visit: - www.cisco.com/go/terms). + ). 3. OWNERSHIP: Cisco retains sole and exclusive ownership of the Sample Code, including all intellectual property rights therein, except with diff --git a/README.md b/README.md index 64d3bd3..7f193bb 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# DNA Center - VLAN / Port provisioning +# Catalyst Center - VLAN / Port provisioning [![published](https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-published.svg)](https://developer.cisco.com/codeexchange/github/repo/gve-sw/gve_devnet_dnac_vlan_provisioning) -This repository contains sample code for deploying interface VLAN configuration to Cisco DNA Center via a simplified web interface. This allows someone to create new VLANs & assign them to switch interfaces, without needing to access DNA center or have knowledge of the underlying device configuration. +This repository contains sample code for deploying interface VLAN configuration to Cisco Catalyst Center via a simplified web interface. This allows someone to create new VLANs & assign them to switch interfaces, without needing to access Catalyst Center or have knowledge of the underlying device configuration. The web application walks through the following workflow: -- Select a DNA center appliance & log in -- Search DNA Center for network switches using a hostname filter +- Select a Catalyst Center appliance & log in +- Search Catalyst Center for network switches using a hostname filter - Use a web form to create new VLANs, then drag & drop interfaces to each VLAN -- Deploy the provided VLAN/interface configuration to the target device via DNA Center templates -- Monitor DNA center template deployment status & view configuration summary +- Deploy the provided VLAN/interface configuration to the target device via Catalyst Center templates +- Monitor Catalyst Center template deployment status & view configuration summary ## Contacts @@ -18,7 +18,7 @@ The web application walks through the following workflow: ## Solution Components -- DNA Center +- Catalyst Center - Catalyst Switching - Python / Flask @@ -36,7 +36,7 @@ git clone pip install -r requirements.txt ``` -### **Step 3 - Provide DNA Center list** +### **Step 3 - Provide Catalyst Center list** In order to use this script, a YAML file of DNAC addresses must be provided. This file must be named `dna-servers.yml`. This file also contains the provisioning project & template names. @@ -78,9 +78,9 @@ A few additional items may be configured via environment variables. Environment There are two authentication modes for this web app, single or multiple. -Single authentication mode is the default and will be used if no other configuration is specified. In this mode, the web app user provides their DNA-C login credentials - and that user account is used for all subsequent API calls to DNA Center. +Single authentication mode is the default and will be used if no other configuration is specified. In this mode, the web app user provides their DNA-C login credentials - and that user account is used for all subsequent API calls to Catalyst Center. -Multiple authentication mode is intended to be used in situations where the end user does not have API write access to DNA Center. In this mode, the user still logs into the web app with their own DNA-C user account. However, these credentials are only used to authenticate to DNA-C & ensure the user has access. All subsequent API calls are performed by a separate DNA-C service account with write access to the API. This ensures that the end user can accomplish the provisioning without their DNA Center account requiring API write privileges. +Multiple authentication mode is intended to be used in situations where the end user does not have API write access to Catalyst Center. In this mode, the user still logs into the web app with their own DNA-C user account. However, these credentials are only used to authenticate to DNA-C & ensure the user has access. All subsequent API calls are performed by a separate DNA-C service account with write access to the API. This ensures that the end user can accomplish the provisioning without their Catalyst Center account requiring API write privileges. Multiple authentication can be configured by setting the following environment variables: @@ -114,7 +114,7 @@ Alternatively, a docker-compose.yml file has been included as well. # Related Sandbox -- [Cisco DNA Center Lab](https://devnetsandbox.cisco.com/RM/Diagram/Index/b8d7aa34-aa8f-4bf2-9c42-302aaa2daafb?diagramType=Topology) +- [Cisco Catalyst Center Lab](https://devnetsandbox.cisco.com/RM/Diagram/Index/b8d7aa34-aa8f-4bf2-9c42-302aaa2daafb?diagramType=Topology) # Screenshots diff --git a/app.py b/app.py index 14462ce..f1b73ab 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,4 @@ -""" Copyright (c) 2023 Cisco and/or its affiliates. +""" Copyright (c) 2024 Cisco and/or its affiliates. This software is licensed to you under the terms of the Cisco Sample Code License, Version 1.1 (the "License"). You may obtain a copy of the License at @@ -13,24 +13,26 @@ """ # Import Section -from flask import Flask, render_template, request, redirect, flash, session -import sys +import os +import re +import secrets import string +import sys from datetime import datetime -import secrets -import yaml -from jinja2 import Environment, FileSystemLoader from time import sleep -import re + +import yaml from dnacentersdk import api from dnacentersdk.exceptions import ApiError -from flask_session import Session from dotenv import load_dotenv -import os +from flask import Flask, redirect, render_template, request, session +from jinja2 import Environment, FileSystemLoader + +from flask_session import Session # Load environment variables load_dotenv() -CUSTOMER_NAME = os.getenv("CUSTOMER_NAME", "Cisco DNA Center") +CUSTOMER_NAME = os.getenv("CUSTOMER_NAME", "Cisco Catalyst Center") # App Mode enables single-credential or multiple-credential authentication to DNA Center. # SINGLEAUTH mode means that the credentials used to log into the web app are also used to push templates to DNA Center. # MULTIAUTH mode allows users with DNAC read-only API access to use this app by providing a separate API write credential @@ -92,6 +94,12 @@ def login(): Collect user credentials & check ability to log into DNAC. """ + # Set up session data + session["deploymentError"] = None + session["deploymentStatus"] = None + session["template_payload"] = None + session["target_device"] = None + # On GET, render login page: if request.method == "GET": # If already authenticated, redirect @@ -137,7 +145,6 @@ def login(): error=errormessage, customer_name=CUSTOMER_NAME, ) - if dnac.access_token != "": # As long we verified we could authenticate once, mark session as authenticated # and send user to VLAN provisioning page @@ -145,7 +152,7 @@ def login(): return redirect("/select-device") else: # If for some reason we don't have an authentication token, return login page & error - errormessage = f"Invalid DNAC access token" + errormessage = "Invalid DNAC access token" session["auth"] = False return render_template( "login.html", @@ -218,21 +225,18 @@ def vlan_provision(): # If form submitted, generate template & upload to DNAC # then push for provisioning if request.method == "POST": - # Generate unique template ID based on session cookie. - # This allows multiple people to use app without overwriting the same template - session["template_uid"] = request.cookies.get("session")[-18:] # Generate / Upload / Deploy template via DNAC generateTemplatePayload(request.json) uploadTemplate( session["template_payload"], devices[session.get("target_device")] ) - deployTemplate(devices[session.get("target_device")]) session["deploymentStatus"] = None + deployTemplate(devices[session.get("target_device")]) # Return status page after deployment is started return redirect("/status") # On first page load, query device interfaces to populate drag & drop - if not "interfaces" in devices[session.get("target_device")].keys(): + if "interfaces" not in devices[session.get("target_device")].keys(): getDeviceInterfaces(session.get("target_device")) return render_template( @@ -313,7 +317,7 @@ def getDNACDevices(filter: str) -> None: device_list[mgtIP]["family"] = device["family"] device_list[mgtIP]["series"] = device["series"] timestamp = datetime.fromtimestamp(device["lastUpdateTime"] / 1000) - device_list[mgtIP]["lastupdate"] = timestamp.strftime(f"%b %d %Y, %I:%M%p") + device_list[mgtIP]["lastupdate"] = timestamp.strftime("%b %d %Y, %I:%M%p") # Query device location for device in device_list: @@ -341,7 +345,7 @@ def getDeviceInterfaces(device_ip: str) -> None: for interface in interfaces["response"]: # Only select GigabitEthernet interfaces on the first module. - # Should skip management interface & NIM slots + # Should skip management interface, NIM slots, and AppGig interface if re.match("^GigabitEthernet\d\/0\/\d", interface["portName"]): # Store interface name to UUID mapping session["dnac_devices"][device_ip]["interfaces"][ @@ -544,10 +548,17 @@ def deployTemplate(device_info: dict) -> None: session["deploymentStatus"] = "inprogress" session["deploymentError"] = None # Deploy template - deploy_template = dnac.configuration_templates.deploy_template( - templateId=session["templateID"], - targetInfo=target_devices, - ) + try: + deploy_template = dnac.configuration_templates.deploy_template( + templateId=session["templateID"], + targetInfo=target_devices, + ) + except ApiError as e: + app.logger.error("Error deploying template: ") + app.logger.error(e) + session["deploymentStatus"] = "fail" + session["deploymentError"] = str(e) + return # Grab deployment UUID session["deploy_id"] = str(deploy_template.deploymentId).split(":")[-1].strip() # If any errors are generated, they are included in the deploymentId field diff --git a/docker-compose.yml b/docker-compose.yml index a7423d0..a64cdb3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: image: ghcr.io/gve-sw/gve_devnet_dnac_vlan_provisioning:latest container_name: gve_devnet_dnac_vlan_provisioning volumes: - - /config_templates:/app/config_templates/ + - ./config_templates:/app/config_templates/ - ./dna-servers.yaml:/app/dna-servers.yaml ports: - "5000:5000" diff --git a/example.env b/example.env new file mode 100644 index 0000000..9e45427 --- /dev/null +++ b/example.env @@ -0,0 +1,3 @@ +APP_MODE= +DNAC_USER= +DNAC_PASS= \ No newline at end of file diff --git a/example_dna-servers.yaml b/example_dna-servers.yaml index b77d331..c3f3936 100644 --- a/example_dna-servers.yaml +++ b/example_dna-servers.yaml @@ -1,10 +1,10 @@ servers: server-name-01: - alias: DNA 01 + alias: Catalyst Center 01 address: 10.10.10.10 server-name-02: - alias: DNA 02 + alias: Catalyst Center 02 address: 10.20.20.20 templates: - project: DNAC_Project + project: Catalyst_Center_Project template: api_port_vlan_config diff --git a/templates/login.html b/templates/login.html index 31a9ac7..f390b59 100644 --- a/templates/login.html +++ b/templates/login.html @@ -30,7 +30,7 @@

-

DNAC VLAN Provisioning

+

VLAN Provisioning

@@ -69,7 +69,7 @@

DNAC VLAN Provisioning

{% endfor %} - + diff --git a/templates/masterPage.html b/templates/masterPage.html index 9712d14..4d0daf0 100644 --- a/templates/masterPage.html +++ b/templates/masterPage.html @@ -8,7 +8,7 @@ - DNAC VLAN Provisioning + Catalyst Center VLAN Provisioning @@ -29,7 +29,7 @@ -

DNAC VLAN Provisioning

+

Catalyst Center VLAN Provisioning

diff --git a/templates/select-device.html b/templates/select-device.html index 5cf6172..cc06c4b 100644 --- a/templates/select-device.html +++ b/templates/select-device.html @@ -71,7 +71,8 @@

Step 1: Filter Devices

Step 2: Select Device

-

Select target device below for provisioning

+

Select target device below for provisioning.

+

Note: Unreachable devices may not be selected.

@@ -94,8 +95,14 @@

Step 2: Select Device

@@ -106,6 +113,8 @@

Step 2: Select Device

{% if device_list[device].reachability == "Reachable" %} + {% else %} + {% endif %} {{ device }}