- RouterOS Script 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| inventory | ||
| network | ||
| playbooks | ||
| roles | ||
| .gitignore | ||
| ansible.cfg | ||
| pyproject.toml | ||
| README.md | ||
| requirements.yml | ||
| uv.lock | ||
picorealm-infra
Purpose
Infrastructure-as-code for the picorealm environment: a homelab modeled on how real organizations (healthcare, government, universities) build and run Windows infrastructure. This repo is the single source of truth for host names, addresses, and configuration.
Build flow for a Windows host: autounattend.xml does only the bootstrap (disk, edition, Administrator password, SSH enabled), then Ansible applies everything else. See ms-server-install-automation.md for the install side.
Conventions
| Item | Value |
|---|---|
| Public domain | picorealm.net (Cloudflare; no records under ad.) |
| AD domain | ad.picorealm.net — single forest, single domain |
| NetBIOS name | PICO |
| UPN suffix | picorealm.net |
| Site code | SFO1 (nearest airport + building number); geography goes in AD Sites, not domain names |
| Hostnames | <SITE>-<ROLE><NN>, max 15 characters (e.g. SFO1-HV01) |
| Time zone | US Mountain Standard Time on all machines |
| Diagnostic data | 1 (Required), matching autounattend.xml |
Tier 0 systems (DCs, PKI, Entra Connect) never run on orange or on any host that serves internet-facing workloads.
Address plan
Home LAN 192.168.12.0/24, gateway BGW320 (.1). The BGW320 can only reserve addresses inside its DHCP pool, so static addresses (servers, AMT) live outside it.
| Range | Use |
|---|---|
.1 |
Gateway (BGW320) |
.2–.19 |
Network infrastructure: Pi-hole .2 (macvlan container on orange), SFO1-SW01 (MikroTik CRS125-24G-1S) .3, Brother printer .18 |
.20–.99 |
Static servers: SFO1-HV01 .21, orange .35, Forgejo .50 (macvlan container on orange) |
.100–.199 |
BGW320 DHCP pool and reservations |
.200–.229 |
Spare |
.230–.239 |
Out-of-band management (Intel AMT, static inside AMT): SFO1-HV01 .231, orange .232 |
.240–.254 |
Spare |
macvlan containers on orange (Pi-hole, Forgejo) are invisible to scans run from orange itself (a macvlan child cannot talk to its parent host), so check free addresses from another machine before assigning one. List them on orange with docker network ls --filter driver=macvlan -q | xargs -r docker network inspect -f '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'.
DNS names for AMT addresses are Pi-hole local records under dunes.ixlo.net (orange-amt, sfo1-hv01-amt). Per-host addresses are recorded in inventory/hosts.yml (ansible_host, amt_ip).
Layout
ansible.cfg
requirements.yml collections: ansible.windows, community.windows, microsoft.ad
pyproject.toml, uv.lock control-node toolchain (ansible-core), managed by uv
inventory/
hosts.yml hosts and groups (windows > hyperv_hosts)
group_vars/all.yml org-wide settings (domain, site, time zone)
group_vars/windows/vars.yml Windows connection + baseline settings
group_vars/windows/vault.yml encrypted secrets (create from vault.yml.example)
playbooks/site.yml windows_baseline on all Windows hosts, hyperv_host on Hyper-V hosts
playbooks/network.yml static management address (windows_mgmt_network role)
roles/windows_baseline/ hostname, time zone, sshd + firewall, WER, telemetry, local admin, PowerShell 7
roles/hyperv_host/ Hyper-V role (vSwitch pending the Realtek NIC)
roles/windows_mgmt_network/ static IP on the adapter matching mgmt_mac, DNS servers
network/sfo1-sw01.rsc SFO1-SW01 baseline RouterOS config (applied with reset-configuration run-after-reset)
Control node setup
Runs from lumina (Ubuntu 24.04 under WSL2 on BACCHARIS, mirrored networking). Keep the clone in the WSL filesystem (e.g. ~/src/picorealm-infra), not under /mnt/c, which is slow and breaks file permissions. Default branch: master.
The control-node toolchain belongs to the repo, so every clone gets the same versions:
- ansible-core is a uv project dependency, pinned in
uv.lock(pyproject.tomldeclares it; Ubuntu's packaged ansible-core is too old for Windows over SSH, officially supported from 2.18). - Collections install into
./collections(collections_pathinansible.cfg; ignored by git), fromrequirements.yml. .venv/is created by uv and ignored by git.
Install sshpass (needed for the first, password-based run), then create the virtual environment from the lock file and install the collections:
sudo apt install sshpass && uv sync && uv run ansible-galaxy collection install -r requirements.yml
Run every Ansible command through uv run (e.g. uv run ansible-playbook ...), or activate the environment once per shell with source .venv/bin/activate. To upgrade ansible-core: uv lock --upgrade-package ansible-core && uv sync, then commit uv.lock.
Create and encrypt the vault, then set vault_windows_admin_password to the host's current Administrator password:
cp inventory/group_vars/windows/vault.yml.example inventory/group_vars/windows/vault.yml && uv run ansible-vault encrypt inventory/group_vars/windows/vault.yml
Set pwsh_version in inventory/group_vars/windows/vars.yml to the current PowerShell LTS release (the play stops with an assertion if it's empty).
First run
SFO1-HV01 still has its DHCP address (.125) until playbooks/network.yml has run, so pass it as mgmt_bootstrap_ip until then. Accept the host key once, so host key checking passes:
ssh Administrator@192.168.12.125 exit
Check connectivity, then apply the baseline:
uv run ansible windows -m ansible.windows.win_ping --ask-vault-pass -e mgmt_bootstrap_ip=192.168.12.125
uv run ansible-playbook playbooks/site.yml --ask-vault-pass -e mgmt_bootstrap_ip=192.168.12.125
The first run renames the host to SFO1-HV01 and reboots it, then reboots again after installing Hyper-V. Both reboots are handled by win_reboot. Re-running the playbook should report no changes; anything that changes on a re-run is drift or a non-idempotent task worth fixing.
Then move the host to its static address (mgmt_ip, .21). Do this near the machine: if it fails, recovery is the local console (sconfig). The change runs as a one-time scheduled task on the host (it would kill the SSH session otherwise); Ansible copies the trusted host key to the new address, reconnects there, and cleans up. The script's log is C:\ProgramData\picorealm\set-mgmt-ip.log.
uv run ansible-playbook playbooks/network.yml --ask-vault-pass -e mgmt_bootstrap_ip=192.168.12.125
Afterwards, drop -e mgmt_bootstrap_ip, remove the .125 reservation on the BGW320, and point the Pi-hole record sfo1-hv01.dunes.ixlo.net at .21.
Switching to SSH keys
Add the control node's public key to windows_admin_ssh_pubkeys in inventory/group_vars/windows/vars.yml and run the playbook. The role writes C:\ProgramData\ssh\administrators_authorized_keys (the file sshd uses for members of Administrators, instead of the user's .ssh folder) and restricts it to Administrators and SYSTEM, without which sshd ignores it. Once key login works, remove ansible_password from vars.yml.
Roadmap
- RTL8125B NIC: driver, external vSwitch with
AllowManagementOS = false; I219-LM stays for management and AMT. - First DCs for
ad.picorealm.net: AD SiteSFO1, UPN suffix, DNS forwarders to a filtering public resolver, conditional forwarders to Pi-hole for self-hosted zones, Pi-hole conditional forward forad.picorealm.net. - Domain join (
microsoft.ad.membership), DNS pointed at the DCs only, Windows LAPS for the local Administrator. - Move telemetry and password policy to GPO; Prometheus
windows_exporterscraped from orange. - Later: member servers on orange's KVM (non-Tier 0, virtio drivers), MECM, Azure Arc.