diff --git a/content/posts/cml-api.md b/content/posts/cml-api.md new file mode 100644 index 0000000..2dd4567 --- /dev/null +++ b/content/posts/cml-api.md @@ -0,0 +1,187 @@ +--- +title: "CCNP Security: Using the Cisco Modeling Labs API with Python" +date: 2025-06-09 +toc: true +tags: + - Network Automation + - Python + - Cisco + - DevOps +--- + +## 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](#full-script)): + +```python +#!/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: + +```python +# 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 + +```python +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: + +```python +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. + +```bash +# 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](https://git.benhays.org) 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 + +```python +#!/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) +```