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
-
Define the Schema: The CUE schema (
router.cue) defines all possible configuration options with types and constraints. -
Create Device Config: The device-specific configuration (
device.cue) uses the schema to define a concrete device configuration. -
Validate: The CUE engine validates the device configuration against the schema before processing.
-
Render Template: The Jinja2 template (
cisco_template.j2) uses the validated device configuration to generate the Cisco config. -
Generate Config: The Python script (
generate.py) ties everything together, loading the CUE data and rendering the template.
Benefits of This Approach
- Validation: CUE ensures all configurations are valid before template rendering.
- Reusability: Templates can be reused across many devices.
- Maintainability: Changes to the schema automatically validate all configurations.
- Documentation: The schema serves as documentation for all configurable options.
- Version Control: All components can be version controlled separately.
Would you like me to elaborate on any particular aspect of this system?