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. 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. 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 1. Meta-Template Structure
-------------------------------------------------------- --------------------------------------------------------