This commit is contained in:
parent
560e9200bb
commit
c2e8c6fb82
187
content/posts/cml-api.md
Normal file
187
content/posts/cml-api.md
Normal file
@ -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)
|
||||
```
|
Loading…
Reference in New Issue
Block a user