Establish clean homelab infrastructure baseline
lint / yaml (push) Has been cancelled
lint / ansible (push) Has been cancelled
lint / terraform (push) Has been cancelled

Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
2026-09-09 16:47:20 +00:00
commit 88a02ababa
418 changed files with 50579 additions and 0 deletions
@@ -0,0 +1,73 @@
---
# VyOS router VM (VM 100) — gateway for the PVE SDN VNets, OSPF peer to the NEC IX.
#
# HOW THIS IS MANAGED — and why not the obvious way:
# We drive `vyos.vyos.vyos_config` with explicit `set` lines from a template,
# NOT the collection's resource modules (vyos_interfaces / vyos_ospfv2 /
# vyos_firewall_rules). Reason, verified 2026-07-25 on this box:
# * VyOS 2025.11 REJECTS the old syntax: `set firewall name X ...` ->
# "Configuration path: firewall [name] is not valid"
# * vyos.vyos 6.0.0 still EMITS `firewall name` (grepped the module_utils)
# So the resource modules would generate config this release cannot parse. On a
# Stream/tech-preview build the safe move is to own the syntax ourselves;
# vyos_config still gives idempotency by diffing against the running config.
vyos_router_hostname: vyos-rtr
# LAN leg — OSPF adjacency with the NEC IX (and the laptop, which is also an
# OSPF speaker on this segment).
vyos_lan_interface: eth0
vyos_lan_address: "192.168.10.2/24"
vyos_lan_gateway: "192.168.10.1"
vyos_nameserver: "192.168.10.5"
vyos_dhcp_nameserver: "192.168.10.5" # the Samba DC — see the flaky-WAN notes
# SDN legs. `vnet` is the PVE VNet the NIC is attached to; `passive` keeps OSPF
# from trying to form adjacencies with guests on these segments.
# `dhcp.subnet_id` MUST be unique across the whole dhcp-server config (VyOS 1.4+
# requires it explicitly). Ranges deliberately start at .100 so .2-.99 stay free
# for anything that wants a static address inside a VNet.
vyos_sdn_interfaces:
- iface: eth1
vnet: labnet
address: "10.60.0.1/24"
network: "10.60.0.0/24"
description: "labnet VLAN100 gateway"
dhcp:
subnet_id: 1
start: "10.60.0.100"
stop: "10.60.0.200"
domain: "ad.ddupan.top"
# Reservations sit BELOW the .100 pool start so they never collide with it.
# Keyed on the VM's pinned MAC (see proxmox/ansible/roles/pve_vm).
reservations:
- { name: retrolab, mac: "bc:24:11:68:a0:51", address: "10.60.0.10" }
- iface: eth2
vnet: retronet
address: "10.61.0.1/24"
network: "10.61.0.0/24"
description: "retronet VLAN110 gateway"
dhcp:
subnet_id: 2
start: "10.61.0.100"
stop: "10.61.0.200"
domain: "ad.ddupan.top"
# Retro Windows (9x/NT/2000) resolves names via NetBIOS, not DNS, so the
# segment needs a WINS server -- this is the whole reason retronet exists.
# Was 192.168.10.5 (the Samba DC); now retro-pdc, which is the PDC of the
# RETRONET domain, so browser elections and domain logons resolve on-segment
# and retronet keeps no dependency on the production DC.
# PRECONDITION: the WINS service must actually be installed and running on
# retro-pdc -- an unanswering wins-server option is worse than the old one.
wins: "10.61.0.5"
# OSPF. The SDN subnets are declared as INTRA-AREA networks, deliberately.
# Do NOT switch this to `redistribute connected`: that advertises every connected
# interface (so any future NIC leaks automatically) and injects E2 routes whose
# metric does not accumulate path cost. Verified on the IX: the same prefix went
# from `O E2 ... [110/20]` to `O ... [110/2]` after this change.
vyos_ospf_router_id: "192.168.10.2"
vyos_ospf_area: "0"
vyos_ospf_networks:
- "192.168.10.0/24"
@@ -0,0 +1,69 @@
---
# Apply the desired VyOS configuration. `vyos_config` compares the rendered
# `set` lines against the running config and issues only the differences, so
# re-runs are no-ops. Supports --check and --diff.
- name: Render the desired configuration
ansible.builtin.set_fact:
_vyos_lines: >-
{{ lookup('template', 'vyos.conf.j2').splitlines()
| map('trim') | reject('equalto', '') | list }}
- name: Apply configuration
vyos.vyos.vyos_config:
lines: "{{ _vyos_lines }}"
# Persist to config.boot; without this the config is lost on reboot.
save: true
# Pull the PRE-change running config back to the control host. VyOS also
# keeps its own commit revisions (`show system commit`, `rollback N`), but
# those are only reachable if the box is still reachable -- which is exactly
# what a bad change takes away.
backup: true
backup_options:
dir_path: "{{ playbook_dir }}/../vyos/backups"
filename: "config.boot"
comment: "ansible {{ lookup('pipe', 'date -u +%Y-%m-%dT%H:%M:%SZ') }}"
register: _vyos_cfg
# `backup: true` fetches the running config every run, which the module counts
# as a change. Report changed ONLY when commands were actually issued,
# otherwise real drift is indistinguishable from a routine backup.
changed_when: (_vyos_cfg.commands | default([]) | length) > 0
- name: Show what changed
ansible.builtin.debug:
msg: "{{ _vyos_cfg.commands | default(['(no changes)']) }}"
# ── post-deploy verification ──────────────────────────────────────────────
# Proving the config was WRITTEN is not the same as proving the network still
# WORKS. These assert operational state, which is the part a bad change breaks.
- name: Collect operational state
vyos.vyos.vyos_command:
commands:
- show ip ospf neighbor
- show interfaces
register: _vyos_state
changed_when: false
- name: Assert OSPF adjacency with the upstream router is Full
ansible.builtin.assert:
that: "'Full' in _vyos_state.stdout[0] and vyos_lan_gateway in _vyos_state.stdout[0]"
fail_msg: >-
No Full OSPF adjacency with {{ vyos_lan_gateway }}. The SDN subnets are
NOT being advertised, so nothing can reach them.
Neighbors seen:\n{{ _vyos_state.stdout[0] }}
success_msg: "OSPF adjacency with {{ vyos_lan_gateway }} is Full"
- name: Assert each SDN gateway address is actually live
ansible.builtin.assert:
that: "item.address in _vyos_state.stdout[1]"
fail_msg: >-
{{ item.iface }} ({{ item.description }}) is missing {{ item.address }} --
guests on that VNet have no gateway.
quiet: true
loop: "{{ vyos_sdn_interfaces }}"
loop_control:
label: "{{ item.iface }} {{ item.address }}"
- name: Report
ansible.builtin.debug:
msg: "{{ _vyos_state.stdout[0].splitlines() | select('search', 'Full') | list }}"
@@ -0,0 +1,50 @@
{# Desired VyOS config as `set` lines. vyos_config diffs these against the
running config, so only differences are applied. Written for VyOS 2025.11
syntax explicitly -- see defaults/main.yml for why we do not use the
collection's resource modules. #}
set system host-name {{ vyos_router_hostname }}
set system name-server {{ vyos_nameserver }}
{# --- LAN leg: OSPF peer with the NEC IX --- #}
set interfaces ethernet {{ vyos_lan_interface }} address {{ vyos_lan_address }}
set interfaces ethernet {{ vyos_lan_interface }} description 'LAN / OSPF to NEC IX'
{# --- SDN legs: gateways for the PVE VNets --- #}
{% for i in vyos_sdn_interfaces %}
set interfaces ethernet {{ i.iface }} address {{ i.address }}
set interfaces ethernet {{ i.iface }} description '{{ i.description }}'
{% endfor %}
{# --- default route out; OSPF carries the rest --- #}
set protocols static route 0.0.0.0/0 next-hop {{ vyos_lan_gateway }}
{# --- OSPF: intra-area, NOT redistribute connected --- #}
set protocols ospf parameters router-id {{ vyos_ospf_router_id }}
{% for n in vyos_ospf_networks %}
set protocols ospf area {{ vyos_ospf_area }} network {{ n }}
{% endfor %}
{% for i in vyos_sdn_interfaces %}
set protocols ospf area {{ vyos_ospf_area }} network {{ i.network }}
{# passive: advertise the subnet, but never try to peer with guests on it #}
set protocols ospf interface {{ i.iface }} passive
{% endfor %}
{# --- management --- #}
set service ssh port 22
{# --- DHCP for the SDN VNets --- #}
{% for i in vyos_sdn_interfaces if i.dhcp is defined %}
set service dhcp-server shared-network-name {{ i.vnet | upper }} subnet {{ i.network }} subnet-id {{ i.dhcp.subnet_id }}
set service dhcp-server shared-network-name {{ i.vnet | upper }} subnet {{ i.network }} option default-router {{ i.address.split('/')[0] }}
set service dhcp-server shared-network-name {{ i.vnet | upper }} subnet {{ i.network }} option name-server {{ vyos_dhcp_nameserver }}
set service dhcp-server shared-network-name {{ i.vnet | upper }} subnet {{ i.network }} option domain-name {{ i.dhcp.domain }}
{% if i.dhcp.wins is defined %}
set service dhcp-server shared-network-name {{ i.vnet | upper }} subnet {{ i.network }} option wins-server {{ i.dhcp.wins }}
{% endif %}
set service dhcp-server shared-network-name {{ i.vnet | upper }} subnet {{ i.network }} range 0 start {{ i.dhcp.start }}
set service dhcp-server shared-network-name {{ i.vnet | upper }} subnet {{ i.network }} range 0 stop {{ i.dhcp.stop }}
{% for r in i.dhcp.reservations | default([]) %}
set service dhcp-server shared-network-name {{ i.vnet | upper }} subnet {{ i.network }} static-mapping {{ r.name }} mac {{ r.mac }}
set service dhcp-server shared-network-name {{ i.vnet | upper }} subnet {{ i.network }} static-mapping {{ r.name }} ip-address {{ r.address }}
{% endfor %}
{% endfor %}