Files

24 KiB

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)

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)

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)

! 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)

#!/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?