# Cisco Router Configuration Template System using CUE and Jinja2 I'll create a system to generate Cisco router configurations using CUE for data modeling and validation, and Jinja2 for template rendering. Here's how we can structure this: ## 1. CUE Schema Definition (router.cue) ```cue package router #Device: { // Basic device information hostname: string model: "ISR4451-X/K9" | "ISR4431" | "ASR1001-X" // etc. serial: string // Location information location: { city: string state: string address1: string address2?: string zipcode: string floor?: string grid?: string rack: string ru: string room: string } // Device role role: "wan-router" | "core-router" | "distribution-router" // Authentication credentials: { enableSecret: string consolePassword: string vtyPassword: string tacacsKey: string snmp: { roCommunity: string rwCommunity: string users: [...{ username: string authType: "sha" | "md5" authKey: string privType: "aes" | "des" privKey: string access: "read" | "write" }] } } // Network services services: { ntp: { servers: [...string] preferredServer: string } dns: { servers: [...string] domain: string } logging: { hosts: [...string] facility: string trapLevel: "informational" | "debug" | "warning" | "error" } } // Interfaces interfaces: { loopback0: { ip: string mask: "255.255.255.255" } wan: { type: "MPLS" | "DMVPN" | "Internet" provider: "AT&T" | "Verizon" | "Lumen" circuitId: string bandwidth: uint ipAddress: string subnetMask: string dot1qVlan?: uint shapingRate: uint } lan: [...{ name: string description: string ipAddress: string subnetMask: string neighbor: string ospf: { area: string authKey: string helloInt: uint deadInt: uint } }] tunnels: [...{ id: uint description: string ipAddress: string subnetMask: string bandwidth: uint nhs: string nbma: string networkId: uint key: uint }] } // Routing protocols routing: { ospf: { processId: uint routerId: string areas: [...{ id: string authType: "message-digest" }] networks: [...{ prefix: string wildcard: string area: string }] } bgp: { asn: uint routerId: string neighbors: [...{ ip: string remoteAs: uint | "internal" description: string password?: string routeMaps: { in?: string out?: string } }] networks: [...{ prefix: string mask: string }] } } // QoS Policy qos: { classes: [...{ name: string match: string bandwidth: string queueLimit?: uint dscp: [...{ value: uint minTh: uint maxTh: uint }] }] shapingRate: uint } // Access control acls: { management: [...{ source: string service: "ssh" | "snmp" | "ntp" | "tacacs" direction: "in" | "out" }] } // Security policies security: { aaa: { methods: [...{ type: "login" | "enable" | "exec" primary: "tacacs+" | "local" fallback?: "local" | "line" }] accounting: [...{ type: "exec" | "commands" | "network" action: "start-stop" method: "tacacs+" }] } passwordPolicy: { minLength: uint maxFail: uint } } } #validate: { // Hostname validation if !strings.HasPrefix(hostname, "RTR-") { "hostname must start with RTR-" } if len(hostname) > 32 { "hostname cannot exceed 32 characters" } if !regexp.Match("[A-Za-z0-9-]+", hostname) { "hostname can only contain alphanumeric characters and hyphens" } // Credential validation if len(credentials.enableSecret) < 8 { "enable secret must be at least 8 characters" } if strings.Contains(credentials.enableSecret, " ") { "enable secret cannot contain spaces" } if len(credentials.consolePassword) < 8 { "console password must be at least 8 characters" } if len(credentials.vtyPassword) < 8 { "vty password must be at least 8 characters" } if len(credentials.tacacsKey) < 16 { "TACACS+ key must be at least 16 characters" } // SNMP validation if len(credentials.snmp.users) == 0 { "at least one SNMP user must be configured" } for _, user in credentials.snmp.users { if len(user.authKey) < 8 { "SNMP auth key must be at least 8 characters for user \(user.username)" } if len(user.privKey) < 8 { "SNMP priv key must be at least 8 characters for user \(user.username)" } } // Interface validation if !net.IsIPv4(interfaces.loopback0.ip) { "loopback0 IP must be a valid IPv4 address" } if interfaces.loopback0.mask != "255.255.255.255" { "loopback0 mask must be 255.255.255.255" } // WAN interface validation if interfaces.wan.bandwidth < 1 || interfaces.wan.bandwidth > 10000 { "WAN bandwidth must be between 1-10000 Mbps" } if !net.IsIPv4(interfaces.wan.ipAddress) { "WAN IP must be a valid IPv4 address" } if !net.IsValidMask(interfaces.wan.subnetMask) { "WAN subnet mask is invalid" } if interfaces.wan.dot1qVlan < 1 || interfaces.wan.dot1qVlan > 4094 { "WAN VLAN must be between 1-4094" } // LAN interface validation for _, iface in interfaces.lan { if !strings.HasPrefix(iface.name, "GigabitEthernet") && !strings.HasPrefix(iface.name, "TenGigabitEthernet") { "LAN interface \(iface.name) must be GigabitEthernet or TenGigabitEthernet" } if !net.IsIPv4(iface.ipAddress) { "LAN interface \(iface.name) IP must be valid IPv4" } if iface.ospf.helloInt < 1 || iface.ospf.helloInt > 65535 { "OSPF hello interval for \(iface.name) must be 1-65535" } if iface.ospf.deadInt < 1 || iface.ospf.deadInt > 65535 { "OSPF dead interval for \(iface.name) must be 1-65535" } if len(iface.ospf.authKey) < 8 { "OSPF auth key for \(iface.name) must be at least 8 characters" } } // Tunnel validation for _, tunnel in interfaces.tunnels { if tunnel.id < 0 || tunnel.id > 2147483647 { "Tunnel ID \(tunnel.id) must be 0-2147483647" } if !net.IsIPv4(tunnel.ipAddress) { "Tunnel \(tunnel.id) IP must be valid IPv4" } if tunnel.bandwidth < 1 || tunnel.bandwidth > 100000 { "Tunnel \(tunnel.id) bandwidth must be 1-100000 Kbps" } if tunnel.networkId < 1 || tunnel.networkId > 4294967295 { "NHRP network ID \(tunnel.networkId) must be 1-4294967295" } } // OSPF validation if routing.ospf.processId < 1 || routing.ospf.processId > 65535 { "OSPF process ID must be 1-65535" } if !net.IsIPv4(routing.ospf.routerId) { "OSPF router ID must be valid IPv4" } if len(routing.ospf.areas) == 0 { "at least one OSPF area must be configured" } for _, area in routing.ospf.areas { if !regexp.Match("^(0|[1-9][0-9]*|[0-9]\\.[0-9]\\.[0-9]\\.[0-9])$", area.id) { "OSPF area ID \(area.id) must be number or IP format" } } // BGP validation if routing.bgp.asn < 1 || routing.bgp.asn > 4294967295 { "BGP ASN must be 1-4294967295" } if !net.IsIPv4(routing.bgp.routerId) { "BGP router ID must be valid IPv4" } if len(routing.bgp.neighbors) == 0 { "at least one BGP neighbor must be configured" } for _, neighbor in routing.bgp.neighbors { if !net.IsIPv4(neighbor.ip) { "BGP neighbor \(neighbor.ip) must be valid IPv4" } } // QoS validation if len(qos.classes) == 0 { "at least one QoS class must be defined" } totalPercent: 0 for _, class in qos.classes { if strings.Contains(class.bandwidth, "percent") { percent: int(strings.Split(class.bandwidth, " ")[2]) totalPercent += percent } } if totalPercent > 100 { "total QoS bandwidth percentages cannot exceed 100% (current: \(totalPercent)%)" } if qos.shapingRate < 1 || qos.shapingRate > 10000 { "shaping rate must be 1-10000 Mbps" } // NTP validation if len(services.ntp.servers) == 0 { "at least one NTP server must be configured" } for _, server in services.ntp.servers { if !net.IsIPv4(server) { "NTP server \(server) must be valid IPv4" } } // DNS validation if len(services.dns.servers) < 2 { "at least two DNS servers should be configured" } for _, server in services.dns.servers { if !net.IsIPv4(server) { "DNS server \(server) must be valid IPv4" } } // Logging validation if len(services.logging.hosts) == 0 { "at least one logging host must be configured" } for _, host in services.logging.hosts { if !net.IsIPv4(host) { "logging host \(host) must be valid IPv4" } } // Security policy validation if security.passwordPolicy.minLength < 8 { "minimum password length must be at least 8" } if security.passwordPolicy.maxFail < 1 || security.passwordPolicy.maxFail > 10 { "max authentication failures must be 1-10" } // AAA method validation if len(security.aaa.methods) == 0 { "at least one AAA authentication method must be configured" } hasLoginMethod: false for _, method in security.aaa.methods { if method.type == "login" { hasLoginMethod = true } } if !hasLoginMethod { "AAA must include a login authentication method" } } ``` ## 2. Example Device Configuration (device.cue) ```cue package router device: #Device & { hostname: "RTR-ATL01-WAN01" model: "ISR4451-X/K9" serial: "FOC12345678" location: { city: "Atlanta" state: "GA" address1: "123 Main St" zipcode: "30303" rack: "RACK-A" ru: "22" room: "MDF-1" } credentials: { enableSecret: "$1$s3cr3t$" consolePassword: "c0ns0l3P@ss" vtyPassword: "vtYP@ssw0rd" tacacsKey: "tacacsKey123" snmp: { roCommunity: "publicRO" rwCommunity: "privateRW" users: [ { username: "admin" authType: "sha" authKey: "authKey123" privType: "aes" privKey: "privKey123" access: "write" }, ] } } services: { ntp: { servers: ["135.89.160.2", "135.89.160.18"] preferredServer: "135.89.160.18" } dns: { servers: ["10.83.108.251", "10.65.72.251"] domain: "example.com" } logging: { hosts: ["10.254.254.238"] facility: "local1" trapLevel: "informational" } } interfaces: { loopback0: { ip: "10.255.255.1" mask: "255.255.255.255" } wan: { type: "MPLS" provider: "AT&T" circuitId: "ABC123456" bandwidth: 1000 ipAddress: "172.30.65.102" subnetMask: "255.255.255.252" dot1qVlan: 249 shapingRate: 1000 } lan: [ { name: "GigabitEthernet0/0/1" description: "Core Switch Connection" ipAddress: "10.1.1.1" subnetMask: "255.255.255.254" neighbor: "CORE-ATL01" ospf: { area: "0" authKey: "ospfKey123" helloInt: 1 deadInt: 4 } }, ] tunnels: [ { id: 11 description: "DMVPN-MPLS to Charlotte" ipAddress: "172.30.4.2" subnetMask: "255.255.254.0" bandwidth: 1000 nhs: "172.30.4.1" nbma: "172.30.65.101" networkId: 200 key: 1002 }, ] } routing: { ospf: { processId: 1 routerId: "10.255.255.1" areas: [ {id: "0", authType: "message-digest"} ] networks: [ {prefix: "10.1.1.0", wildcard: "0.0.0.1", area: "0"}, {prefix: "10.255.255.1", wildcard: "0.0.0.0", area: "0"} ] } bgp: { asn: 65401 routerId: "10.255.255.1" neighbors: [ { ip: "172.30.4.1" remoteAs: "internal" description: "DMVPN Headend" routeMaps: { in: "BGP_INBOUND_FROM_DMVPN" out: "BGP_OUTBOUND_TO_DMVPN" } }, ] networks: [ {prefix: "10.1.1.0", mask: "255.255.255.0"}, {prefix: "10.255.255.1", mask: "255.255.255.255"} ] } } qos: { classes: [ { name: "VOICE_PAYLOAD" match: "dscp ef cs5" bandwidth: "priority percent 30" }, { name: "INTERACTIVE_VIDEO" match: "dscp cs4 af41 af42 af43" bandwidth: "bandwidth remaining percent 15" queueLimit: 300 dscp: [ {value: 32, minTh: 300, maxTh: 300}, {value: 34, minTh: 150, maxTh: 250}, ] }, ] shapingRate: 1000 } security: { aaa: { methods: [ {type: "login", primary: "tacacs+", fallback: "local"}, {type: "enable", primary: "tacacs+", fallback: "enable"}, ] accounting: [ {type: "exec", action: "start-stop", method: "tacacs+"}, {type: "commands", action: "start-stop", method: "tacacs+"}, ] } passwordPolicy: { minLength: 8 maxFail: 5 } } } ``` ## 3. Jinja2 Template (cisco_template.j2) ```jinja2 ! Generated from template on {{ now() }} ! Device: {{ device.hostname }} ! Location: {{ device.location.city }}, {{ device.location.state }} hostname {{ device.hostname }} ! boot system bootflash:isr4400-universalk9.16.09.05.SPA.bin ! service password-encryption no ip domain lookup ip domain name {{ device.services.dns.domain }} ! {% for server in device.services.dns.servers %} ip name-server {{ server }} {% endfor %} ! ! AAA Configuration aaa new-model aaa authentication attempts login {{ device.security.passwordPolicy.maxFail }} ! {% for method in device.security.aaa.methods %} aaa authentication {{ method.type }} default group {{ method.primary }}{% if method.fallback %} {{ method.fallback }}{% endif %} {% endfor %} ! {% for accounting in device.security.aaa.accounting %} aaa accounting {{ accounting.type }} default {{ accounting.action }} group {{ accounting.method }} {% endfor %} ! ! SNMP Configuration {% for user in device.credentials.snmp.users %} snmp-server user {{ user.username }} {{ user.access }} v3 auth {{ user.authType }} {{ user.authKey }} priv {{ user.privType }} {{ user.privKey }} {% endfor %} ! ! NTP Configuration ntp source Loopback0 ntp server {{ device.services.ntp.preferredServer }} prefer {% for server in device.services.ntp.servers %} {% if server != device.services.ntp.preferredServer %} ntp server {{ server }} {% endif %} {% endfor %} ! ! Logging Configuration logging source-interface Loopback0 logging trap {{ device.services.logging.trapLevel }} logging facility {{ device.services.logging.facility }} {% for host in device.services.logging.hosts %} logging host {{ host }} {% endfor %} ! ! Interface Configuration interface Loopback0 description Loopback Address ip address {{ device.interfaces.loopback0.ip }} {{ device.interfaces.loopback0.mask }} ! interface GigabitEthernet0/0/0 description {{ device.interfaces.wan.provider }} MPLS - {{ device.interfaces.wan.circuitId }} bandwidth {{ device.interfaces.wan.bandwidth }} no ip address ! interface GigabitEthernet0/0/0.{{ device.interfaces.wan.dot1qVlan }} description {{ device.interfaces.wan.provider }} MPLS - {{ device.interfaces.wan.circuitId }} encapsulation dot1Q {{ device.interfaces.wan.dot1qVlan }} ip address {{ device.interfaces.wan.ipAddress }} {{ device.interfaces.wan.subnetMask }} service-policy output SHAPE_{{ device.interfaces.wan.shapingRate }}M ! {% for lan in device.interfaces.lan %} interface {{ lan.name }} description {{ lan.description }} ip address {{ lan.ipAddress }} {{ lan.subnetMask }} ip ospf authentication message-digest ip ospf message-digest-key 1 md5 {{ lan.ospf.authKey }} ip ospf hello-interval {{ lan.ospf.helloInt }} ip ospf dead-interval {{ lan.ospf.deadInt }} ! {% endfor %} ! {% for tunnel in device.interfaces.tunnels %} interface Tunnel{{ tunnel.id }} description {{ tunnel.description }} bandwidth {{ tunnel.bandwidth }} ip address {{ tunnel.ipAddress }} {{ tunnel.subnetMask }} ip nhrp network-id {{ tunnel.networkId }} ip nhrp nhs {{ tunnel.nhs }} nbma {{ tunnel.nbma }} multicast tunnel key {{ tunnel.key }} ! {% endfor %} ! ! OSPF Configuration router ospf {{ device.routing.ospf.processId }} router-id {{ device.routing.ospf.routerId }} {% for area in device.routing.ospf.areas %} area {{ area.id }} authentication {{ area.authType }} {% endfor %} {% for net in device.routing.ospf.networks %} network {{ net.prefix }} {{ net.wildcard }} area {{ net.area }} {% endfor %} ! ! BGP Configuration router bgp {{ device.routing.bgp.asn }} bgp router-id {{ device.routing.bgp.routerId }} {% for net in device.routing.bgp.networks %} network {{ net.prefix }} mask {{ net.mask }} {% endfor %} {% for neighbor in device.routing.bgp.neighbors %} neighbor {{ neighbor.ip }} remote-as {{ neighbor.remoteAs }} neighbor {{ neighbor.ip }} description {{ neighbor.description }} {% if neighbor.routeMaps.in %} neighbor {{ neighbor.ip }} route-map {{ neighbor.routeMaps.in }} in{% endif %} {% if neighbor.routeMaps.out %} neighbor {{ neighbor.ip }} route-map {{ neighbor.routeMaps.out }} out{% endif %} {% if neighbor.password %} neighbor {{ neighbor.ip }} password {{ neighbor.password }}{% endif %} {% endfor %} ! ! QoS Configuration policy-map WAN_QOS_OUTBOUND {% for class in device.qos.classes %} class {{ class.name }} {{ class.bandwidth }} {% if class.queueLimit %} queue-limit {{ class.queueLimit }} packets{% endif %} {% for dscp in class.dscp %} random-detect dscp {{ dscp.value }} {{ dscp.minTh }} {{ dscp.maxTh }} {% endfor %} {% endfor %} ! policy-map SHAPE_{{ device.qos.shapingRate }}M class class-default shape average {{ device.qos.shapingRate }}000000 service-policy WAN_QOS_OUTBOUND ! ! Banner banner motd ^ ******************************************************************************** * Warning Notice * * * * This system is restricted solely to authorized users for legitimate * * business purposes only. * ******************************************************************************** ^ ! line con 0 password {{ device.credentials.consolePassword }} login local ! line vty 0 15 password {{ device.credentials.vtyPassword }} login local transport input ssh ! end ``` ## 4. Generation Script (generate.py) ```python #!/usr/bin/env python3 import argparse import jinja2 import json import datetime from cue import load def now(): return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") def main(): parser = argparse.ArgumentParser(description="Generate Cisco configurations from CUE definitions") parser.add_argument("cue_file", help="Input CUE file with device definition") parser.add_argument("template_file", help="Jinja2 template file") parser.add_argument("-o", "--output", help="Output file", default="output.cfg") args = parser.parse_args() # Load CUE data cue_data = load.cue(args.cue_file) device = cue_data["device"] # Set up Jinja2 environment env = jinja2.Environment(loader=jinja2.FileSystemLoader('.')) env.globals['now'] = now template = env.get_template(args.template_file) # Render template config = template.render(device=device) # Write output with open(args.output, 'w') as f: f.write(config) print(f"Configuration generated and saved to {args.output}") if __name__ == "__main__": main() ``` ## 5. Workflow Explanation 1. **Define the Schema**: The CUE schema (`router.cue`) defines all possible configuration options with types and constraints. 2. **Create Device Config**: The device-specific configuration (`device.cue`) uses the schema to define a concrete device configuration. 3. **Validate**: The CUE engine validates the device configuration against the schema before processing. 4. **Render Template**: The Jinja2 template (`cisco_template.j2`) uses the validated device configuration to generate the Cisco config. 5. **Generate Config**: The Python script (`generate.py`) ties everything together, loading the CUE data and rendering the template. ## Benefits of This Approach 1. **Validation**: CUE ensures all configurations are valid before template rendering. 2. **Reusability**: Templates can be reused across many devices. 3. **Maintainability**: Changes to the schema automatically validate all configurations. 4. **Documentation**: The schema serves as documentation for all configurable options. 5. **Version Control**: All components can be version controlled separately. Would you like me to elaborate on any particular aspect of this system?