Update tech_docs/networking/iac_github_project.md

This commit is contained in:
2025-08-02 14:42:04 -05:00
parent f2fbe07dd7
commit 1b4ff4d36b

View File

@@ -1,6 +1,634 @@
Below is a vendor-agnostic, scalable-template review written in “meta-config” form.
It is intentionally abstract (no literal values, no vendor CLI) so it can be mechanically translated to any NOS or rendered by an automation pipeline.
--------------------------------------------------------
1. Meta-Template Structure
--------------------------------------------------------
┌─ object: device
│ ├─ role: head-end-dmvpn-hub
│ ├─ platform: <vendor-agnostic>
│ └─ lifecycle: golden-template → instance-template → device-config
└─
Each stanza below is a YAML-ish block that can be turned into:
- Jinja2 / Ansible variables
- Terraform schema
- OpenConfig YANG
- TTP/TTK parser
--------------------------------------------------------
2. Inventory & Naming
--------------------------------------------------------
inventory:
site_id: "{{ site_id }}" # AAA-BBB-CCC-NNN
function: headend
routing_domain: "{{ rd_index }}"
hostname_pattern: "{{ site_id }}-{{ function }}-{{ sequence }}"
--------------------------------------------------------
3. OS / Image Management
--------------------------------------------------------
image:
golden_version: "{{ lookup('golden_db', platform) }}"
fallback_version: "{{ golden_version | fallback }}"
boot_order: [primary, secondary, usb]
--------------------------------------------------------
4. Global Service Knobs
--------------------------------------------------------
global:
service:
tcp_keepalives: { in: true, out: true }
timestamps: { debug: msec, log: msec, tz: local }
password_encryption: true
sequence_numbers: true
counters_max_age: 10
dhcp: false
pad: false
--------------------------------------------------------
5. Security Baseline
--------------------------------------------------------
security:
auth_failure_rate: 3
password_policy:
min_length: 8
complexity: high
aaa:
method_order: [tacacs, local]
accounting: start-stop
sources:
- { ip: "{{ tacacs_vip }}", vrf: mgmt }
secrets:
enable: "{{ vault.encrypted(enable_secret) }}"
snmp:
version: 3
auth: sha
priv: aes-128
acl: "{{ snmp_acl }}"
--------------------------------------------------------
6. VRF & Loopback Plan
--------------------------------------------------------
vrfs:
- name: mgmt
rd: "{{ site_id }}:1"
interfaces: [MgmtEth0/0/0]
- name: dmvpn
rd: "{{ site_id }}:2"
interfaces: [Loopback-DMVPN, Tunnel-*]
loopbacks:
- name: system
vrf: default
mask: /32
- name: tunnel_source
vrf: dmvpn
mask: /32
--------------------------------------------------------
7. Underlay Interfaces
--------------------------------------------------------
underlay:
uplinks:
- id: 1
type: p2p
media: ethernet
mtu: 9216
vrf: default
ospf: { area: 0, auth: md5, hello: 1, dead: 4 }
- id: 2
type: p2p
media: ethernet
mtu: 9216
vrf: dmvpn
ospf: { area: 0, auth: md5, hello: 1, dead: 4 }
--------------------------------------------------------
8. Overlay (DMVPN) Definition
--------------------------------------------------------
overlay:
type: dmvpn-hub
tunnel_ifs:
- id: 1
src_loopback: tunnel_source
vrf: dmvpn
mtu: 1400
tcp_mss: 1360
nhrp:
auth: "{{ nhrp_key }}"
net_id: "{{ site_id }}"
holdtime: 600
shortcut: true
redirect: true
ipsec:
profile: dmvpn_profile
transform: { enc: aes256-gcm, pfs: group20 }
bgp_listen_range: "{{ tunnel_net }}"
bgp_peer_group:
name: spokes
asn: "{{ bgp_asn }}"
rr_client: true
next_hop_self: true
send_default: true
max_peers: "{{ spoke_limit }}"
--------------------------------------------------------
9. QoS Framework
--------------------------------------------------------
qos:
classifier:
- { name: voice, dscp: ef }
- { name: interactive_vid, dscp: [af41,af42,af43] }
- { name: critical_data, dscp: [af31,af32,af33] }
- { name: business_data, dscp: [af21,af22,af23] }
- { name: bulk_data, dscp: [af11,af12,af13] }
- { name: scavenger, dscp: cs1 }
- { name: net_mgmt, dscp: cs2 }
shaper:
- parent: physical
cir: "{{ circuit_bw }}"
child_policy: per_class
per_class:
voice: { priority_pct: 30 }
interactive_vid: { bw_pct: 15, wred: true }
critical_data: { bw_pct: 20, wred: true }
business_data: { bw_pct: 25, wred: true }
bulk_data: { bw_pct: 10, wred: true }
scavenger: { bw_pct: 5, wred: true }
class_default: { bw_pct: 20, fair_queue: true }
--------------------------------------------------------
10. NetFlow / Telemetry
--------------------------------------------------------
telemetry:
exporter:
- { dst: "{{ collector_vip }}", vrf: mgmt, dscp: af21, proto: udp/9996 }
cache:
active_timeout: 60
inactive_timeout: 15
fields:
- { match: [ipv4_src, ipv4_dst, tos, proto, port_src, port_dst, direction] }
- { collect: [bytes, pkts, first_seen, last_seen, next_hop] }
--------------------------------------------------------
11. Routing Policy
--------------------------------------------------------
policy:
ospf:
areas:
0: { auth: md5, type: p2p_only }
default_originate: true
bgp:
local_as: "{{ bgp_asn }}"
communities:
- { name: blackhole, pattern: "65400:666" }
- { name: transit_nyc, pattern: "65400:1111" }
- { name: transit_clt, pattern: "65400:2222" }
- { name: transit_brm, pattern: "65400:3333" }
route_maps:
- { name: deny_default, seq: 10, action: deny, match: prefix=0.0.0.0/0 }
- { name: spokes_in, seq: 20, action: permit }
- { name: spokes_out, seq: 10, action: permit, set: [community=local_site] }
--------------------------------------------------------
12. Monitoring & SLA
--------------------------------------------------------
sla:
icmp_probes:
- { target: "{{ anycast_gw }}", src: tunnel_1, freq: 180, owner: dmvpn }
logging:
hosts:
- { ip: "{{ loghost1 }}", vrf: mgmt, facility: local1 }
- { ip: "{{ loghost2 }}", vrf: mgmt, facility: local1 }
levels:
- { trap: informational, origin_id: loopback }
--------------------------------------------------------
13. Hardening & ACLs
--------------------------------------------------------
acl:
mgmt_plane:
- { src: "{{ mgmt_net }}", dst: device, proto: ssh }
- { src: "{{ mgmt_net }}", dst: device, proto: snmp }
control_plane:
- { src: "{{ spoke_net }}", dst: device, proto: ipsec }
- { src: "{{ spoke_net }}", dst: device, proto: nhrp }
data_plane:
- { name: undesirable_udp, action: drop, rate: 8kbps }
--------------------------------------------------------
14. Automation Hooks
--------------------------------------------------------
automation:
pre_checks:
- validate_image_md5
- validate_crypto_modulus
post_checks:
- bgp_established
- ipsec_sa_count
- sla_probe_up
drift_detection:
- periodic: "{{ 24h }}"
- source_of_truth: git
- remediation: on_change
--------------------------------------------------------
15. Extensibility Notes
--------------------------------------------------------
- All IP literals live in inventory or vault; template never embeds them.
- All BGP communities, NHRP keys, SNMP strings are vaulted and templated.
- QoS class names & DSCP values are constants; per-link CIR is dynamic.
- The entire config is rendered in three artifacts:
1. golden-template.yaml (this document)
2. per-site variables (YAML or JSON)
3. vendor renderer (Jinja2, YDK, NAPALM, etc.)
This single source-of-truth can therefore drive Cisco IOS-XE, Juniper vSRX, Aruba CX, OpenBSD, or a virtual container image without further human touch.
---
Yes—the structure you showed is **exactly** the next logical layer under the vendor-agnostic “meta-template” I sketched.
Think of the meta-template as the **contract** (what must exist, how it behaves), and your `config_components/` tree as the **concrete implementation** that an automation engine (Ansible, Salt, Nornir, etc.) consumes to stamp out per-device artifacts.
Below is a mapping that keeps the two layers coherent and keeps every artifact reusable across chassis types or vendors.
--------------------------------------------------------
1. Directory ↔ Meta-Template Mapping
--------------------------------------------------------
config_components/
├── core_settings/
│ ├── 00_licensing.j2 ← global.image (golden version, feature licences)
│ ├── 10_system_settings.j2 ← global.service knobs, hostname, NTP, banner
│ └── 20_aaa.j2 ← security.aaa, tacacs sources, local users
├── network_services/
│ ├── 30_vlans.j2 ← vrfs.* (L3 VNIs) + any underlay VLANs
│ └── 40_routing.j2 ← policy.ospf, policy.bgp, route-maps, NHRP
├── interfaces/
│ ├── 50_port_profiles/
│ │ ├── access_port.j2 ← not used on a DMVPN hub, but kept for reuse
│ │ └── trunk_port.j2 ← likewise reusable
│ └── 60_interface_assignments.j2
│ ← underlay.*, overlay.tunnel_ifs, QoS service-policy attachment
├── policies/
│ ├── 70_qos.j2 ← qos.classifier + qos.shaper + qos.per_class
│ └── 80_access_lists.j2 ← acl.mgmt_plane + acl.control_plane + ACL fragments
└── data/
├── site-01.yml ← inventory + per-site variable overlay
└── site-02.yml
--------------------------------------------------------
2. Data-file Inheritance Model
--------------------------------------------------------
data/site-XX.yml
├── inherits: "dmvpn_hub.yml" # shared across all hubs
├── overlays:
│ ├── hostname: "{{ site_id }}-hub-01"
│ ├── tunnel_net: "10.130.108.0/25"
│ ├── circuit_bw: "1 Gbps"
│ └── bgp_asn: 65400
└── secrets: "@vault/dmvpn/{{ site_id }}"
The meta-template (YAML in my previous reply) becomes the **schema** that every data file must satisfy; the J2 files become the **renderer**.
--------------------------------------------------------
3. Rendering Flow
--------------------------------------------------------
1. Inventory script loads `data/site-01.yml`
2. Validates against meta-template schema (pydantic, jsonschema, or yamale)
3. Renders each `.j2` file in numerical order into a single running-config stanza
4. Post-render hooks (linting, drift check, push via Nornir/Napalm)
--------------------------------------------------------
4. Vendor-Neutral Abstraction Tips
--------------------------------------------------------
- Keep **no literals** inside `.j2`; every value is `{{ var }}` or `{{ var | default }}`.
- Use **macro includes** for multi-vendor syntax:
`{% import 'vendor/ios.j2' as syntax %}` vs `{% import 'vendor/junos.j2' %}`
- Store **feature flags** in the data file so the same J2 can be skipped on platforms that lack a capability (e.g., crypto-isakmp on Cisco, security-ike on JunOS).
- If a component is unused (e.g., access_port.j2 on an ASR), the data file simply omits that key and the macro renders nothing.
--------------------------------------------------------
5. Quick Starter Skeleton
--------------------------------------------------------
config_components/
├── _macros/
│ ├── vendor/
│ │ ├── ios.j2
│ │ └── junos.j2
├── core_settings/
│ └── … (as above)
├── network_services/
│ └── …
└── data/
├── dmvpn_hub.yml ← shared variables & schema
├── site-01.yml ← per-site override
└── site-02.yml
With this, the **meta-template** remains the single source of truth, your **config_components/** tree remains tidy, and any future chassis or vendor swap is a one-line change in the renderer macro—not a rewrite of the logic.
---
Below is the **vendor-agnostic YAML fragment** that maps every CLI-level knob you listed to a single, automation-friendly variable path.
No literals, no vendor syntax—just the contract the Jinja2 (or any renderer) will later expand into IOS, JunOS, EOS, etc.
```yaml
global_system:
hostname: "{{ inventory_hostname }}"
boot:
image: "{{ golden_image | default('unset') }}"
config_register: "{{ config_register | default('0x2102') }}"
secrets:
enable: "{{ vault.encrypted(enable_secret) }}"
clock:
timezone: "{{ tz_name | default('UTC') }}"
offset: "{{ tz_offset | default(0) }}"
summer_time:
rules: "{{ summer_rules | default('none') }}" # e.g., 'usa', 'eu', 'recurring'
domain:
name: "{{ dns_domain }}"
name_servers: "{{ dns_servers | default([]) }}"
logging:
buffered:
size: "{{ log_buffer_bytes | default(1048576) }}"
console: "{{ log_console | default(false) }}"
monitor: "{{ log_monitor | default('informational') }}"
hosts:
- { ip: "{{ log_host_1 }}", vrf: "{{ log_vrf | default('mgmt') }}",
proto: "{{ log_proto | default('udp') }}", port: "{{ log_port | default(514) }}" }
- { ip: "{{ log_host_2 }}", vrf: "{{ log_vrf | default('mgmt') }}",
proto: "{{ log_proto | default('udp') }}", port: "{{ log_port | default(514) }}" }
```
Usage notes:
- `vault.encrypted(...)` is a Jinja2 filter that pulls the secret from the vault at render time.
- `inventory_hostname` is injected by Ansible/Nornir.
- The lists (`dns_servers`, `hosts`) are idempotent arrays—renderers can loop (`{% for %}`) to generate one CLI stanza per element.
---
Below is a **deployment-ready YAML scaffold** that expands every knob in your latest checklist into **parameter-only** form—no literals, no CLI, just the variables an automation engine will resolve at render time.
Copy this into `data/<site>.yml`, fill in the blanks, and feed it to the Jinja2 fragments you already keep in `config_components/`.
```yaml
###############################################################################
# AAA
###############################################################################
aaa:
new_model: true
tacacs:
servers:
- { ip: "{{ tacacs1 }}", vrf: "{{ mgmt_vrf }}", key: "{{ vault.tacacs_key }}" }
- { ip: "{{ tacacs2 }}", vrf: "{{ mgmt_vrf }}", key: "{{ vault.tacacs_key }}" }
group_name: "{{ tacacs_group | default('GTAC') }}"
auth_login_default: "{{ aaa_login_method | default(['group tacacs', 'local']) }}"
auth_enable_default: "{{ aaa_enable_method | default(['group tacacs', 'enable']) }}"
auth_commands_15: "{{ aaa_cmd_method | default(['group tacacs', 'local']) }}"
accounting_commands_15:
type: "{{ acct_type | default('start-stop') }}"
group: "{{ acct_group | default('GTAC') }}"
local_users:
- { name: "{{ local_admin }}", priv: 15, secret: "{{ vault.local_secret }}" }
###############################################################################
# VRFs
###############################################################################
vrfs:
- name: "{{ mgmt_vrf }}"
rd: "{{ site_id }}:1"
af: [ipv4, ipv6]
- name: "{{ dmvpn_vrf }}"
rd: "{{ site_id }}:2"
af: [ipv4]
###############################################################################
# CRYPTO
###############################################################################
crypto:
keyring:
name: "{{ keyring_name }}"
vrf: "{{ dmvpn_vrf }}"
psk_list:
- { ip: "{{ spoke_range }}", key: "{{ vault.psk }}" }
isakmp:
policy:
id: "{{ isakmp_policy_id | default(10) }}"
encr: "{{ isakmp_encr | default('aes') }}"
auth: "{{ isakmp_auth | default('pre-share') }}"
group: "{{ isakmp_group | default('14') }}"
ipsec:
transform:
name: "{{ ipsec_transform }}"
esp: "{{ ipsec_esp | default('esp-aes 256 esp-sha-hmac') }}"
mode: "{{ ipsec_mode | default('transport') }}"
profile:
name: "{{ ipsec_profile }}"
pfs: "{{ ipsec_pfs | default(false) }}"
idle_time: "{{ ipsec_idle | default(60) }}"
###############################################################################
# DMVPN TUNNEL
###############################################################################
tunnel:
id: "{{ tunnel_id | default(1) }}"
ip: "{{ tunnel_ip }}"
mask: "{{ tunnel_mask }}"
source: "{{ tunnel_source_intf }}"
mode: "{{ tunnel_mode | default('gre multipoint') }}"
key: "{{ tunnel_key }}"
vrf: "{{ dmvpn_vrf }}"
ipsec_profile: "{{ ipsec_profile }}"
nhrp:
net_id: "{{ nhrp_net_id }}"
auth: "{{ nhrp_key }}"
server_only: true
shortcut: true
redirect: true
tcp_mss: "{{ tunnel_tcp_mss | default(1360) }}"
qos_pre_classify: true
output_policy: "{{ qos_policy_out | default('WAN_QOS_OUTBOUND') }}"
###############################################################################
# ROUTING OSPF
###############################################################################
ospf:
- pid: "{{ ospf_pid_uplink | default(1) }}"
vrf: "{{ mgmt_vrf }}"
rid: "{{ loopback0_ip }}"
ref_bw: "{{ ospf_ref_bw | default(100000) }}"
passive_default: true
areas:
- id: "{{ ospf_area_uplink | default(0) }}"
auth: message-digest
networks:
- { prefix: "{{ uplink_net }}", mask: "{{ uplink_wildcard }}", passive: false }
- pid: "{{ ospf_pid_dmvpn | default(22) }}"
vrf: "{{ dmvpn_vrf }}"
rid: "{{ tunnel_rid }}"
areas:
- id: "{{ ospf_area_dmvpn | default(0) }}"
auth: message-digest
networks:
- { prefix: "{{ tunnel_net }}", mask: "{{ tunnel_wildcard }}", passive: false }
###############################################################################
# ROUTING BGP
###############################################################################
bgp:
asn: "{{ bgp_asn }}"
rid: "{{ loopback0_ip }}"
log_changes: true
neighbors:
- { ip: "{{ ebgp_peer1 }}", remote_as: "{{ ebgp_as1 }}", desc: "To-VPN-Core-1",
ebgp_multihop: 2, update_source: "{{ loopback0_intf }}", pw: "{{ vault.bgp_pw }}" }
- { ip: "{{ ebgp_peer2 }}", remote_as: "{{ ebgp_as2 }}", desc: "To-VPN-Core-2",
ebgp_multihop: 2, update_source: "{{ loopback0_intf }}", pw: "{{ vault.bgp_pw }}" }
listen:
range: "{{ spoke_range }}"
peer_group: "{{ spoke_pg | default('REMOTE-OFFICE') }}"
limit: "{{ spoke_limit | default(300) }}"
af_ipv4:
networks:
- { prefix: "{{ local_summary_net }}", mask: "{{ local_summary_mask }}" }
peer_policies:
- group: "{{ spoke_pg }}"
activate: true
rr_client: true
next_hop_self_all: true
default_originate: true
soft_reconfig: true
route_map_in: "{{ rm_spoke_in }}"
route_map_out: "{{ rm_spoke_out }}"
###############################################################################
# QOS
###############################################################################
qos:
class_maps:
- name: VOICE
match: [dscp ef]
- name: INTERACTIVE_VIDEO
match: [dscp cs4, dscp af41-af43]
- name: CRITICAL_DATA
match: [dscp af31-af33]
- name: BUSINESS_DATA
match: [dscp af21-af23]
- name: BULK_DATA
match: [dscp af11-af13]
- name: SCAVENGER
match: [dscp cs1]
policy_maps:
- name: "{{ qos_policy_out }}"
classes:
- { name: VOICE, priority_pct: 30 }
- { name: INTERACTIVE_VIDEO, bw_remaining_pct: 15, wred: true }
- { name: CRITICAL_DATA, bw_remaining_pct: 20, wred: true }
- { name: BUSINESS_DATA, bw_remaining_pct: 25, wred: true }
- { name: BULK_DATA, bw_remaining_pct: 10, wred: true }
- { name: SCAVENGER, bw_remaining_pct: 5, wred: true }
- { name: class-default, bw_remaining_pct: 20, fair_queue: true }
###############################################################################
# ACL / PREFIX / COMMUNITY
###############################################################################
acls:
standard:
- name: "{{ snmp_acl }}"
rules:
- { action: permit, src: "{{ mgmt_net1 }}" }
- { action: permit, src: "{{ mgmt_net2 }}" }
extended:
- name: "{{ qos_acl_bulk }}"
rules: "{{ qos_rules_bulk | default([]) }}"
prefix_lists:
- name: DEFAULT
rules:
- { seq: 5, action: permit, prefix: 0.0.0.0/0 }
community_lists:
- name: BLACKHOLE
type: expanded
rules:
- { action: permit, regex: "_666$" }
###############################################################################
# SNMP
###############################################################################
snmp:
location: "{{ snmp_location }}"
contact: "{{ snmp_contact }}"
groups:
- { name: NOC_RO, version: 3, sec: authPriv, read_view: VIEW-STD }
users:
- { name: "{{ snmp_user }}", group: NOC_RO,
auth: sha, auth_pw: "{{ vault.snmp_auth }}",
priv: aes128, priv_pw: "{{ vault.snmp_priv }}" }
hosts:
- { ip: "{{ snmp_trap_host }}", vrf: "{{ mgmt_vrf }}",
version: 3, user: "{{ snmp_user }}" }
traps:
enable: "{{ snmp_traps | default(['snmp','bgp','ospf','link']) }}"
###############################################################################
# INTERFACES (underlay + mgmt)
###############################################################################
interfaces:
mgmt:
- name: "{{ mgmt_intf }}"
vrf: "{{ mgmt_vrf }}"
ip: "{{ mgmt_ip }}"
mask: "{{ mgmt_mask }}"
uplinks:
- name: "{{ uplink1_intf }}"
desc: "To Core-1"
ip: "{{ uplink1_ip }}"
mask: "{{ uplink1_mask }}"
ospf: { area: "{{ ospf_area_uplink }}", cost: 10 }
qos_out: "{{ qos_policy_out }}"
- name: "{{ uplink2_intf }}"
desc: "To Core-2"
ip: "{{ uplink2_ip }}"
mask: "{{ uplink2_mask }}"
ospf: { area: "{{ ospf_area_uplink }}", cost: 10 }
qos_out: "{{ qos_policy_out }}"
###############################################################################
# SLA
###############################################################################
sla:
probes:
- { type: icmp-echo, target: "{{ anycast_gw }}",
src: "{{ tunnel_intf }}", freq: 180, owner: DMVPN }
###############################################################################
# CONTROL-PLANE POLICING / MISC
###############################################################################
control_plane:
policy_in: "{{ cpp_policy | default('drop-udp') }}"
line:
con0:
exec_timeout: "{{ con_timeout | default('10 0') }}"
vty:
exec_timeout: "{{ vty_timeout | default('10 0') }}"
transport: "{{ vty_transport | default('ssh') }}"
acl_in: "{{ vty_acl | default('SECURE_ACCESS') }}"
```
Save this file as `data/<site>.yml`, merge it with the vault, and render.
The Jinja2 fragments in `config_components/` need only reference the keys above—no further CLI details leak into the YAML.
---
Below is a vendor-agnostic, scalable-template review written in “meta-config” form.
It is intentionally abstract (no literal values, no vendor CLI) so it can be mechanically translated to any NOS or rendered by an automation pipeline.
--------------------------------------------------------
1. Meta-Template Structure
--------------------------------------------------------