From 1b4ff4d36b2730f005e634e8dabd592003c4d3ae Mon Sep 17 00:00:00 2001 From: medusa Date: Sat, 2 Aug 2025 14:42:04 -0500 Subject: [PATCH] Update tech_docs/networking/iac_github_project.md --- tech_docs/networking/iac_github_project.md | 628 +++++++++++++++++++++ 1 file changed, 628 insertions(+) diff --git a/tech_docs/networking/iac_github_project.md b/tech_docs/networking/iac_github_project.md index 5b62e15..0f85b2d 100644 --- a/tech_docs/networking/iac_github_project.md +++ b/tech_docs/networking/iac_github_project.md @@ -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: +│ └─ 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/.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/.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 --------------------------------------------------------