CCNP Security: Using the Cisco Modeling Labs API with Python

  ·   4 min read

Introduction

As I’ve been working through my SAUTO 300-735 exam preparation, I’ve found myself spending quite a bit of time in Cisco Modeling Labs (CML). While the web interface is perfectly functional for most tasks, I kept finding myself wanting a quicker way to get an overview of my lab topologies and export device configurations without clicking through multiple sub-menus and pages.

The CML REST API provides functionally identical capabilities as the GUI interface, so it was fairly easy to automate common tasks like listing nodes and working with node configurations.

The Code

The script is fairly straightforward but covers the essential workflow for interacting with CML programmatically. Here’s a few code exerpts for reference (find the full script here):

#!/usr/bin/env python3
import requests
import sys
import json
import urllib3
import argparse

USERNAME = 'developer'
PASSWORD = 'C1sco12345'
URL = 'https://10.10.20.161'

I’ve hardcoded some default values for my lab environment, but the script also accepts command-line arguments to override these.

The authentication flow follows the standard CML API pattern:

# Authenticate and get a token
token_response = requests.post(f"{URL}/api/v0/authenticate", 
                               json={'username': USERNAME, 'password': PASSWORD},
                               headers=headers,
                               verify=False)
headers['Authorization'] = f"Bearer {token_response.text.strip('\"')}"

Once authenticated, the script can make subsequent API calls using the bearer token.

Interactive Lab Selection

CML uses GUID-like lab IDs for programmatic access; the script lists each lab ID and its associated name for the user to select

if not lab_id:
    labs = requests.get(f"{URL}/api/v0/labs", headers=headers, verify=False)
    labs = labs.json()

    print("Labs:")
    for lab in labs:
        print(f"- {lab}: {requests.get(f'{URL}/api/v0/labs/{lab}', headers=headers, verify=False).json()['lab_title']}")
    
    lab_id = input("Enter the lab ID to fetch inventory: ").strip()

Configuration Export

The --export-configs flag adds the ability to automatically save device configurations to local files:

if args.export_configs:
    with open(f'{node_info['label']}_config.txt', 'w') as f:
        f.write(node_info['configuration'])
    print(f"  Configuration exported to {node_info['label']}_config.txt")

Real-World Usage

The full script uses the argparse library to handle more complex uses and avoid the hardcoded information.

# Quick inventory check
python3 cml-inventory.py

# Export all configs from a specific lab
python3 cml-inventory.py --lab a1b2c3d4-e5f6-7890 --export-configs

# Use with different credentials
python3 cml-inventory.py --username admin --password MyPassword

Conclusion

The script, along with my other CCNP security materials, should be soon available on my Gitea server if you’d like to try it out or build upon it. As always, remember that this script is for lab/testing environments only - TLS checks are disabled, authentication is hard-coded, et cetera.

Full Script

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This script fetches the inventory of a specified lab in Cisco Modeling Labs (CML).

import requests
import sys
import json
import urllib3
import argparse

USERNAME = 'developer'
PASSWORD = 'C1sco12345'
URL = 'https://10.10.20.161'

if __name__ == "__main__":
    # hide warnings about insecure requests
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    # Parse command line arguments
    parser = argparse.ArgumentParser(description='Fetch CML lab inventory.')
    parser.add_argument('--username', type=str, default=USERNAME, help='CML username')
    parser.add_argument('--password', type=str, default=PASSWORD, help='CML password')
    parser.add_argument('--url', type=str, default=URL, help='CML URL')
    parser.add_argument('--lab', type=str, help='Lab ID to fetch inventory for')
    parser.add_argument('--export-configs', action='store_true', help='Export node configurations')
    args = parser.parse_args()

    USERNAME = args.username
    PASSWORD = args.password
    URL = args.url
    
    if args.lab:
        lab_id = args.lab
    else:
        lab_id = None

    try:
        headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        }

        print(f"--- {URL} - CML {requests.get(f'{URL}/api/v0/system_information', verify=False, headers=headers).json()['version']} ---")
            
        # Authenticate and get a token
        token_response = requests.post(f"{URL}/api/v0/authenticate", 
                                       json={'username': USERNAME, 'password': PASSWORD},
                                       headers=headers,
                                       verify=False)  # Disable SSL verification for demo purposes
        token_response.raise_for_status() # Raises an exception if the request failed
        if token_response is None or token_response.status_code != 200:
            print("Authentication failed")
            sys.exit(1)
        headers['Authorization'] = f"Bearer {token_response.text.strip('\"')}"

        if not lab_id:
            labs = requests.get(f"{URL}/api/v0/labs", headers=headers, verify=False)
            labs.raise_for_status()
            labs = labs.json()

            print("Labs:")
            for lab in labs:
                print(f"- {lab}: {requests.get(f'{URL}/api/v0/labs/{lab}', headers=headers, verify=False).json()['lab_title']}")
            
            # Ask user for lab ID
            lab_id = input("Enter the lab ID to fetch inventory: ").strip()
            if not lab_id:
                print("No lab ID provided.")
                sys.exit(1)

        # Fetch inventory for the specified lab
        inventory_response = requests.get(f"{URL}/api/v0/labs/{lab_id}/nodes", headers=headers, verify=False)
        inventory_response.raise_for_status()
        inventory = inventory_response.json()

        print(f"\nInventory for lab {lab_id}:")
        
        for node in inventory:
            node_info = requests.get(f'{URL}/api/v0/labs/{lab_id}/nodes/{node}', headers=headers, verify=False).json()
            print(f"- {node_info['label']} ({node_info['node_definition']})")
            if args.export_configs:
                with open(f'{node_info['label']}_config.txt', 'w') as f:
                    f.write(node_info['configuration'])
                print(f"  Configuration exported to {node_info['label']}_config.txt")

    except requests.exceptions.RequestException as e:
        print(f"Error fetching inventory: {e}")
        sys.exit(1)