โ† Back to posts
AI & Automation

Using Claude to Automate Cisco ACI via the APIC REST API

ACI Automation with Claude

Cisco ACIโ€™s APIC exposes a comprehensive REST API over HTTPS that gives programmatic access to everything โ€” tenants, VRFs, bridge domains, EPGs, contracts, L3Outs, and more. The challenge is that ACIโ€™s object model is deep and complex. Building the right JSON payload for a new tenant from scratch requires knowing exactly which managed objects to create and in what order.

Claude bridges this gap. Describe what you need in plain English, get correctly structured APIC API payloads back. Describe a connectivity problem, get a structured diagnosis. This post covers practical patterns for AI-assisted ACI management.


Setup

import anthropic
import requests
import json
import urllib3

urllib3.disable_warnings()  # Suppress self-signed cert warnings for lab

client = anthropic.Anthropic()

class APICSession:
    def __init__(self, host: str, username: str, password: str):
        self.base = f"https://{host}/api"
        self.session = requests.Session()
        self.session.verify = False
        self._login(username, password)
    
    def _login(self, username: str, password: str):
        payload = {"aaaUser": {"attributes": {"name": username, "pwd": password}}}
        r = self.session.post(f"{self.base}/aaaLogin.json", json=payload)
        r.raise_for_status()
        token = r.json()["imdata"][0]["aaaLogin"]["attributes"]["token"]
        self.session.headers.update({"Cookie": f"APIC-cookie={token}"})
    
    def get(self, path: str, **params) -> list:
        r = self.session.get(f"{self.base}/{path}", params=params)
        r.raise_for_status()
        return r.json().get("imdata", [])
    
    def post(self, path: str, payload: dict) -> dict:
        r = self.session.post(f"{self.base}/{path}", json=payload)
        r.raise_for_status()
        return r.json()

def ask_claude(prompt: str, system: str = None) -> str:
    kwargs = {
        "model": "claude-opus-4-5",
        "max_tokens": 3000,
        "messages": [{"role": "user", "content": prompt}]
    }
    if system:
        kwargs["system"] = system
    return client.messages.create(**kwargs).content[0].text

Pattern 1: Natural Language to ACI Tenant Policy

ACIโ€™s object model requires creating a precise hierarchy: Tenant โ†’ VRF โ†’ BD โ†’ EPG โ†’ Contract. Claude handles the JSON structure:

def generate_tenant_policy(description: str) -> dict:
    prompt = f"""
Generate a Cisco ACI APIC REST API JSON payload to create the following tenant configuration.
Return a valid APIC POST payload for /api/mo/uni.json

Configuration request:
{description}

Requirements:
- Use proper ACI object model hierarchy (fvTenant โ†’ fvCtx โ†’ fvBD โ†’ fvAp โ†’ fvAEPg)
- Include all required attributes (name, descr, dn, rn)
- Add appropriate contracts (vzBrCP) with subjects and filters for described connectivity
- Set BD unicastRoute and arpFlood appropriately
- Include relations (fvRsCtx, fvRsDomAtt, fvRsBd, fvRsProv, fvRsCons)

Output ONLY valid JSON that can be POST'd directly to the APIC. No explanation.
"""
    return json.loads(ask_claude(
        prompt,
        system="You are a Cisco ACI expert. Generate only valid APIC REST API JSON payloads."
    ))

# Example: create a full tenant for a web application
payload = generate_tenant_policy("""
Create tenant 'WEB-APP-PROD' with:
- VRF: WEB-VRF
- Bridge Domain: WEB-BD with subnet 10.10.10.1/24, unicast routing enabled
- Application Profile: WEB-APP
- Three EPGs: WEB-TIER (web servers), APP-TIER (app servers), DB-TIER (databases)
- Contracts: 
  * USERS-TO-WEB: allow TCP 80,443 from outside to WEB-TIER
  * WEB-TO-APP: allow TCP 8080 from WEB-TIER to APP-TIER  
  * APP-TO-DB: allow TCP 5432 (PostgreSQL) from APP-TIER to DB-TIER
- Associate all EPGs with VMware VMM domain 'vCenter-DVS'
""")

apic = APICSession("apic1.example.com", "admin", "password")
result = apic.post("mo/uni.json", payload)
print(f"Created: {result}")

Pattern 2: Endpoint Connectivity Troubleshooter

ACI connectivity issues are notoriously hard to debug manually. Claude reasons through the policy model:

def troubleshoot_endpoint_connectivity(
    apic: APICSession,
    src_ip: str,
    dst_ip: str
) -> dict:
    # Gather endpoint data from APIC
    src_endpoints = apic.get(
        "node/class/fvCEp.json",
        **{"query-target-filter": f'eq(fvCEp.ip,"{src_ip}")'}
    )
    dst_endpoints = apic.get(
        "node/class/fvCEp.json",
        **{"query-target-filter": f'eq(fvCEp.ip,"{dst_ip}")'}
    )
    
    # Get EPG details for both endpoints
    src_epg_dn = src_endpoints[0]["fvCEp"]["attributes"].get("epgDn", "") if src_endpoints else ""
    dst_epg_dn = dst_endpoints[0]["fvCEp"]["attributes"].get("epgDn", "") if dst_endpoints else ""
    
    # Get contracts between EPGs
    contracts = []
    if src_epg_dn and dst_epg_dn:
        contracts = apic.get(f"mo/{src_epg_dn}/rscons.json")
    
    # Get fault data
    faults = apic.get("node/class/faultInst.json", **{
        "query-target-filter": "eq(faultInst.severity,\"major\")",
        "page-size": "20"
    })
    
    prompt = f"""
Troubleshoot ACI connectivity from {src_ip} to {dst_ip}.

Source endpoint data:
{json.dumps(src_endpoints, indent=2)}

Destination endpoint data:
{json.dumps(dst_endpoints, indent=2)}

Contracts from source EPG:
{json.dumps(contracts, indent=2)}

Active major faults:
{json.dumps(faults[:5], indent=2)}

Return JSON with:
- connectivity_possible: true/false/unknown
- root_cause: specific reason connectivity is failing (or "No issues found")
- policy_path: description of the EPG โ†’ Contract โ†’ EPG path
- missing_elements: list of missing contracts, filters, or relations
- recommended_fixes: list of specific APIC changes needed
- verification_steps: APIC CLI commands to verify after fix

Output ONLY valid JSON.
"""
    return json.loads(ask_claude(prompt))

apic = APICSession("apic1.example.com", "admin", "password")
result = troubleshoot_endpoint_connectivity(apic, "10.10.10.5", "10.10.20.5")
print(f"Connectivity possible: {result['connectivity_possible']}")
print(f"Root cause: {result['root_cause']}")
for fix in result['recommended_fixes']:
    print(f"  Fix: {fix}")

Pattern 3: ACI Configuration Audit

def audit_tenant(apic: APICSession, tenant_name: str) -> dict:
    # Pull comprehensive tenant config
    tenant_data = {
        "tenant": apic.get(f"mo/uni/tn-{tenant_name}.json", **{"rsp-subtree": "full", "rsp-subtree-depth": "4"}),
        "faults": apic.get("node/class/faultInst.json", **{
            "query-target-filter": f'wcard(faultInst.dn,"tn-{tenant_name}")',
        }),
        "epgs_without_contracts": apic.get("node/class/fvAEPg.json", **{
            "query-target-filter": f'wcard(fvAEPg.dn,"tn-{tenant_name}")',
            "rsp-subtree": "children",
            "rsp-subtree-class": "fvRsProv,fvRsCons",
        }),
    }
    
    prompt = f"""
Audit this Cisco ACI tenant configuration for best practice violations and security issues.

Tenant: {tenant_name}
Data: {json.dumps(tenant_data, indent=2)}

Return JSON with:
- security_score: integer 0-100
- violations: list of dicts with 'severity', 'finding', 'recommendation'
- orphaned_objects: list of objects with no meaningful connections
- unused_contracts: contracts defined but not consumed
- epgs_without_domain: EPGs missing physical/VMM domain association
- summary: one paragraph assessment

Output ONLY valid JSON.
"""
    return json.loads(ask_claude(prompt))

Pattern 4: L3Out Generator

L3Out configuration is one of ACIโ€™s most complex areas. Claude handles the full object hierarchy:

def generate_l3out(params: dict) -> dict:
    prompt = f"""
Generate an ACI L3Out APIC REST API payload for /api/mo/uni/tn-{params['tenant']}.json

L3Out Parameters:
- Tenant: {params['tenant']}
- VRF: {params['vrf']}
- L3Out name: {params['name']}
- Routing protocol: {params['protocol']} 
- External EPG name: {params['ext_epg_name']}
- External subnets: {params['external_subnets']}
- Connected router IP: {params['router_ip']}
- ACI node/interface: {params['node_interface']}
- OSPF area: {params.get('ospf_area', '0')}

Include all required objects:
l3extOut โ†’ l3extRsEctx โ†’ l3extLNodeP โ†’ l3extLIfP โ†’ 
l3extRsPathL3OutAtt โ†’ l3extInstP โ†’ l3extSubnet

Output ONLY valid JSON payload.
"""
    return json.loads(ask_claude(
        prompt,
        system="You are a Cisco ACI expert. Generate only valid APIC REST API JSON."
    ))

l3out_payload = generate_l3out({
    "tenant": "PROD",
    "vrf": "PROD-VRF", 
    "name": "L3OUT-INTERNET",
    "protocol": "OSPF",
    "ext_epg_name": "INTERNET-EXT",
    "external_subnets": ["0.0.0.0/0"],
    "router_ip": "192.168.1.2/30",
    "node_interface": "topology/pod-1/node-101/pathep-[eth1/1]",
    "ospf_area": "0.0.0.0"
})

Pattern 5: Daily Fabric Health Report

def daily_health_report(apic: APICSession) -> str:
    data = {
        "faults": apic.get("node/class/faultInst.json", **{
            "query-target-filter": "ne(faultInst.severity,\"cleared\")",
            "order-by": "faultInst.severity|desc",
            "page-size": "50"
        }),
        "nodes": apic.get("node/class/fabricNode.json"),
        "pods": apic.get("node/class/fabricPod.json"),
        "tenant_count": len(apic.get("node/class/fvTenant.json")),
        "epg_count": len(apic.get("node/class/fvAEPg.json")),
    }
    
    prompt = f"""
Generate a concise daily health report for this Cisco ACI fabric.

Fabric Data:
{json.dumps(data, indent=2)}

Write a professional report with these sections:
1. Executive Summary (2-3 sentences)
2. Fault Summary (count by severity, top 3 most critical)
3. Infrastructure Status (node health, pod status)
4. Tenant Overview (counts, any tenants with issues)
5. Recommended Actions (prioritized)

Use plain English, no JSON. Format as a readable report.
"""
    return ask_claude(prompt)

apic = APICSession("apic1.example.com", "admin", "password")
report = daily_health_report(apic)
print(report)
# Email this report, post to Slack, store in ticketing system, etc.
// Found this useful? Share it or start a conversation.