Skip to main content
Version: Next

Router Services Configuration

The Router Box runs the foundational infrastructure services required before deploying OpenStack. It runs sequentially across four host groups:

PlayHost GroupServiceGate Variable
Provision OpenWRT VMrouter_hostCreates the router VM on KVMrouter_provisioning: true
Configure OpenWRTopenwrt_routersDHCP, DNS, TFTP, WireGuard, BGProuter_provisioning: true
Setup PXE Serveropenwrt_routersPXE boot and firmware filesrouter_provisioning: true
Initialize Environmentkolla_bastionDocker registry for OpenStack imagesAlways runs
Deploy HedgeHog Controllerhedgehog_hostFabric controller VMAlways runs
Provision HedgeHog Fabrichedgehog_controlSwitch and VPC configurationAlways runs

All OpenWrt plays are gated behind router_provisioning. Set it to true in your inventory to enable router provisioning:

router_host:
vars:
router_provisioning: true

To deploy a new change done in the inventory or during an upgrade, you need to run the phoenix platform-setup.sh installer:

$ platform-setup.sh --bootstrap

OpenWrt Router — DHCP Server

The router runs dnsmasq as a DHCP server, providing IP address assignment to infrastructure nodes. It supports per-interface pools with configurable ranges, static MAC-to-IP leases, and the ability to disable dynamic allocation on specific interfaces.

Variables

VariableTypeDefaultDescription
openwrt_dhcp_poolshostlistSee belowDHCP pools per network interface
openwrt_dhcp_leaseslist[]Static DHCP reservations (MAC to IP)

Each entry in openwrt_dhcp_poolshost:

FieldTypeDescription
namestringPool identifier
interfacestringOpenWrt interface name to serve DHCP on
startstringFirst address in the pool (host part only)
endstringLast address in the pool (host part only)
dynamicdhcpstring"1" to allow dynamic leases, "0" to only serve static leases
forcestring"1" to force DHCP even if another server is detected
ignorestring"1" to disable DHCP on this interface entirely

Each entry in openwrt_dhcp_leases:

FieldTypeDescription
namestringHostname for the lease (used in DNS if dns: "1")
macstringMAC address (e.g., "88:e9:a4:02:37:96")
ipstringIP address, optionally with CIDR (e.g., "10.30.0.10" or "10.30.0.1/16")
dnsstring"1" to register a DNS entry for this host

Default DHCP Pools

The role provides these defaults. Override them in your inventory to match your network layout:

openwrt_dhcp_poolshost:
- name: "lan"
interface: "lan"
ignore: "1" # DHCP disabled on LAN
- name: "wan"
interface: "wan"
ignore: "1" # DHCP disabled on WAN
- name: "frontend"
interface: "frontend"
start: "100"
end: "250"
dynamicdhcp: "0" # Only static leases
force: "1"

Example

openwrt_routers:
vars:
openwrt_dhcp_poolshost:
- name: "frontend"
interface: "frontend"
start: "100"
end: "250"
dynamicdhcp: "0"
force: "1"
- name: "provisioning"
interface: "provisioning"
start: "100"
end: "250"
dynamicdhcp: "1"
force: "1"

openwrt_dhcp_leases:
# OpenStack control node
- name: "control0"
dns: "1"
mac: "88:e9:a4:02:37:96"
ip: "10.30.0.101"
# Ceph storage nodes
- name: "storage0"
dns: "1"
mac: "b8:ce:f6:7e:10:b2"
ip: "10.30.0.63"
- name: "storage1"
dns: "1"
mac: "B8:CE:F6:7E:11:CA"
ip: "10.30.0.62"
# Hedgehog controller
- name: "hedgehog0"
dns: "1"
mac: "02:7f:3c:a9:4d:e2"
ip: "192.168.33.249"

OpenWrt Router — DNS Resolver

Dnsmasq also acts as the local DNS resolver. It provides a local domain for internal name resolution, static DNS A record overrides, and domain rebind exemptions for split-horizon DNS setups.

Variables

VariableTypeDefaultDescription
openwrt_dhcp_dnsmasq_localstringLocal domain prefix for DNS (e.g., "/mydomain.com/")
openwrt_dhcp_dnsmasq_domainstringDomain name assigned to DHCP clients (e.g., "mydomain.com")
openwrt_dhcp_dnsmasq_rebind_domainlist[]Domains exempt from DNS rebind protection
openwrt_dhcp_hostoverrideslist[]Static DNS A record overrides

Each entry in openwrt_dhcp_hostoverrides:

FieldTypeDescription
namestringHostname to resolve (e.g., "control0")
ipstringIP address the hostname resolves to

The local and domain variables work together: local: "/mydomain.com/" tells dnsmasq to answer queries for mydomain.com locally, and domain: "mydomain.com" assigns that domain to DHCP clients.

The rebind_domain list is needed when upstream DNS returns private IPs for certain domains (e.g., office VPN domains). Without this exemption, dnsmasq rejects these responses as potential DNS rebind attacks.

Example

openwrt_routers:
vars:
openwrt_dhcp_dnsmasq_local: "/mydomain.com/"
openwrt_dhcp_dnsmasq_domain: "mydomain.com"
openwrt_dhcp_dnsmasq_rebind_domain:
- "mydomain.com" # Allow office router queries resolving to private IPs

openwrt_dhcp_hostoverrides:
# OpenStack API VIP
- name: "openstack-vip"
ip: "10.30.0.222"
# IaaS Console
- name: "console"
ip: "192.168.104.104"
# Management services (resolved to K8s ingress IP)
- name: "console.mydomain.com"
ip: "192.168.104.23"
- name: "grafana.mydomain.com"
ip: "192.168.104.23"

OpenWrt Router — TFTP/PXE Boot Server

The router serves as a PXE boot server for bare metal provisioning. Dnsmasq provides the TFTP service and tells PXE clients which boot file to load. Boot files (kernel, initrd, GRUB configs) are served from a shared filesystem mounted from the KVM host via 9P virtio.

Variables

VariableTypeDefaultDescription
openwrt_dhcp_dnsmasq_enable_tftpstring"1"Enable TFTP server in dnsmasq
openwrt_dhcp_dnsmasq_tftp_rootstring"/tftp"Root directory for TFTP file serving
openwrt_dhcp_dnsmasq_dhcp_bootstring"bootx64.efi"Boot filename sent to PXE clients via DHCP option 67
openwrt_config.http_rootstring"/www/boot"HTTP server root for boot files (kernel, initrd, cloud-init)
openwrt_config.tftp_rootstring"/tftp"TFTP root directory (symlinked to shared filesystem)

How It Works

  1. A bare metal server sends a PXE DHCP request on the provisioning network
  2. Dnsmasq responds with the router's IP as the TFTP server and bootx64.efi as the boot file
  3. The server downloads the GRUB bootloader via TFTP
  4. GRUB loads its per-MAC configuration, kernel, and initrd via HTTP from the router
  5. The installer boots with cloud-init user-data served from the HTTP root

The pxe_boot role (which runs after openwrt_configure in the bootstrap playbook) populates the TFTP and HTTP directories with the actual boot files. The TFTP/PXE variables here control the dnsmasq side of the handshake.

Example

The defaults work for most deployments. Override only if you need non-standard paths:

openwrt_routers:
vars:
openwrt_dhcp_dnsmasq_enable_tftp: "1"
openwrt_dhcp_dnsmasq_tftp_root: "/tftp"
openwrt_dhcp_dnsmasq_dhcp_boot: "bootx64.efi"
openwrt_config:
http_root: "/www/boot"
tftp_root: "/tftp"

OpenWrt Router — WireGuard VPN

The router can terminate WireGuard VPN tunnels for site-to-site connectivity (e.g., to an upstream datacenter) and remote operator access. Configuration has two parts: defining the WireGuard interface and adding peers to it.

WireGuard Interface

WireGuard interfaces are defined inside openwrt_network_interfaceshost alongside regular network interfaces. Set proto: "wireguard" to create a WireGuard interface.

FieldTypeDescription
protostringMust be "wireguard"
wg_managekeysbooleanfalse = keys stored in inventory (vault-encrypted)
wg_listen_portintegerUDP port for incoming WireGuard connections
wg_addresseslistIP addresses assigned to this interface (CIDR notation)
wg_private_keystringInterface private key (must be vault-encrypted)

WireGuard Peers

Peers are defined in openwrt_network_wireguardpeers. This variable is a dictionary where each key is a peer name.

FieldTypeRequiredDescription
interfacestringYesWireGuard interface name this peer belongs to
managekeysbooleanYesfalse = keys stored in inventory
descriptionstringNoHuman-readable description
allowed_ipslistYesCIDR blocks routed through this peer
public_keystringYesPeer's WireGuard public key
endpoint_hoststringNoPeer's hostname or IP (omit for listen-only peers)
endpoint_portintegerNoPeer's UDP port (required if endpoint_host is set)
persistent_keepaliveintegerNoSeconds between keepalive packets (use 25 for NAT traversal)

Peers without endpoint_host are listen-only — the router waits for them to connect. This is typical for operator VPN clients.

This feature is conditional: it only activates when openwrt_network_wireguardpeers is defined in inventory.


OpenWrt Router — Bird BGP Routing

The router can run Bird3 as a BGP daemon for dynamic routing. This is used to advertise public IP prefixes (floating IPs) to upstream routers and to receive host routes from OpenStack Neutron's dynamic routing agent.

This feature is conditional: it only activates when bird_bgp_router_id is defined in inventory.

Variables

VariableTypeDefaultDescription
bird_bgp_router_idstring"10.0.1.1"BGP router ID (must be unique in the BGP domain)
bird_bgp_local_asinteger65000Local BGP Autonomous System number
bird_bgp_announced_prefixeslist[]Network prefixes to advertise (CIDR notation)
bird_bgp_direct_interfaceslist[]Interfaces whose connected routes are imported into Bird
bird_bgp_neighborslist[]BGP neighbor/peer definitions

Each entry in bird_bgp_neighbors:

FieldTypeRequiredDescription
namestringYesNeighbor identifier (used in Bird config as protocol name)
ipstringYesNeighbor's IP address
remote_asintegerYesNeighbor's AS number (same AS = iBGP, different = eBGP)
importlistNoImport filter rules. Omit or set to [] to import none
exportlistNoExport filter rules. Omit or set to [] to export none

Import/export filter rules are Bird filter expressions. Common patterns:

  • "net = 119.15.113.1/32" — match a specific prefix
  • "net ~ [ 119.15.113.0/24{24,32} ]" — match a prefix range
  • proto = "openstack_*" — match routes learned from a specific protocol
  • ifname = "eth2.104" — match routes on a specific interface

Typical Topology

Upstream ISP (eBGP) <---> Edge Router (eBGP/iBGP) <---> OpenWrt Router (iBGP) <---> OpenStack Neutron DR Agent

The OpenWrt router sits between the edge router and OpenStack:

  • It receives floating IP host routes from Neutron via iBGP
  • It advertises the public subnet to the edge router
  • The edge router peers with the upstream ISP

Example

openwrt_routers:
hosts:
router-0:
bird_bgp_local_as: 65253
bird_bgp_router_id: "10.30.0.1"
bird_bgp_announced_prefixes:
- "119.15.113.0/24"
bird_bgp_direct_interfaces:
- "eth2.104" # External VLAN interface

bird_bgp_neighbors:
# iBGP to edge router via WireGuard tunnel
- name: ty15_router0
ip: "172.19.0.252"
remote_as: 55393
import:
- "net = 119.15.113.1/32"
export:
- proto = "openstack_*"
- ifname = "eth2.104"

# iBGP to OpenStack Neutron (receives floating IP host routes)
- name: openstack_control0
ip: "10.30.0.101"
remote_as: 65253
import:
- "net ~ [ 119.15.113.0/24{24,32} ]"
export: []

Deployment Host

The deployment host (inventory group kolla_bastion) is the machine from which OpenStack is installed and managed. The bootstrap playbook sets up a local Docker registry on this host so that OpenStack container images can be served locally, enabling air-gapped deployments and faster image pulls across the cluster.

This play always runs (no gate variable).

Inventory Group

The kolla_bastion group defines connection credentials for the deployment host:

kolla_bastion:
vars:
ansible_user: ubuntu
ansible_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...encrypted...
ansible_become_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...encrypted...
ansible_ssh_private_key_file: "~/.ssh/mido_infra.pem"
hosts:
192.168.33.250: {}

Docker Registry Variables

VariableTypeDefaultDescription
registry_imagestringdocker.io/library/registryContainer image for the registry
registry_image_tagstring"2"Registry image tag
registry_portinteger5000Port the registry listens on
registry_data_dirstring/var/lib/registryPersistent storage directory
registry_configure_insecurebooleantrueRegister as insecure in Podman's registries.conf
registry_populate_imagesbooleanfalseAutomatically populate registry with OpenStack images
registry_source_registrystringghcr.ioSource registry to pull images from
registry_source_namespacestringmidokura/openstack.kollaSource namespace
registry_image_tag_kollastring2025.1-ubuntu-nobleKolla image tag to mirror
registry_image_listlist91 imagesList of image names to populate
registry_image_list_filestring""Path to a file with image names (overrides registry_image_list)
registry_dest_namespacestringopenstack.kolla/Namespace in the local registry
registry_populate_parallelinteger5Number of concurrent image copy operations
registry_source_authdict{}Credentials for the source registry ({username: "", password: ""})

When registry_populate_images: true, the role uses skopeo to copy all images from the source registry into the local registry. This is useful for air-gapped environments or to avoid rate limits.

Example

Minimal configuration (registry with defaults, no image population):

kolla_bastion:
vars:
ansible_user: ubuntu
ansible_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...encrypted...
ansible_become_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...encrypted...
hosts:
192.168.33.250: {}

With image population from GHCR:

kolla_bastion:
vars:
ansible_user: ubuntu
ansible_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...encrypted...
ansible_become_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...encrypted...
registry_populate_images: true
registry_source_auth:
username: "github-user"
password: "ghp_xxxxxxxxxxxx"
hosts:
192.168.33.250: {}

Hedgehog Controller VM

The Hedgehog controller manages the network fabric (switches, VLANs, VPCs). The bootstrap playbook provisions it as a KVM virtual machine on the hedgehog_host with PCI passthrough for the management NIC that connects to the physical switches.

This play always runs on the hedgehog_host group.

Variables

The role uses the merge pattern: defaults are defined in hedgehog_defaults and you override specific fields via the hedgehog dictionary in inventory. Unspecified fields keep their defaults.

VariableTypeDefaultDescription
hedgehog.vm_namestringhedgehog-controllerLibvirt VM name
hedgehog.ram_mbinteger16384RAM in megabytes (16 GB)
hedgehog.vcpuinteger8Number of virtual CPUs
hedgehog.disk_gbinteger40Disk size in gigabytes
hedgehog.iso_urlstring(built-in URL)URL to the Hedgehog installer ISO
hedgehog.iso_checksumstring(built-in checksum)SHA256 checksum for ISO verification
hedgehog.iso_dirstring/var/lib/libvirt/images/isosDirectory to cache the downloaded ISO
hedgehog.autostartbooleantrueAuto-start VM on host reboot
hedgehog.bridge_namestringvirbr-libvirtHost bridge for upstream VM connectivity
hedgehog.vm_macstring02:7f:3c:a9:4d:e2MAC address of the VM's bridge interface
hedgehog.mgmt_nic_pcistringpci_0000_01_00_1PCI address of the NIC to pass through for switch management

The mgmt_nic_pci is the most important variable to override: it must point to a physical NIC on the host that is cabled to the management switch where the fabric switches are connected. Use the pci_XXXX_XX_XX_X format (underscores instead of colons/dots).

The iso_url must point to a signed URL (e.g., Azure Blob Storage SAS URL) for the Hedgehog installer image. The built-in default may expire — check the expiration date and update as needed.

Provisioning Process

  1. The ISO is downloaded to iso_dir and verified against iso_checksum
  2. A VM is created with UEFI boot, the specified resources, and a CDROM attached to the ISO
  3. The management NIC is passed through to the VM via PCI hostdev
  4. The VM boots the installer, which runs unattended
  5. After installation completes, the role waits for the Kubernetes API inside the VM to become ready (up to ~100 minutes)

Example

In most cases you only need to override iso_url and mgmt_nic_pci:

hedgehog_host:
hosts:
bastion0:
hedgehog:
iso_url: "https://storage.example.com/hedgehog-installer.iso?token=xxx"
mgmt_nic_pci: "pci_0000_58_00_0"

Full override example:

hedgehog_host:
hosts:
bastion0:
hedgehog:
vm_name: hedgehog-controller
ram_mb: 32768
vcpu: 16
disk_gb: 80
iso_url: "https://storage.example.com/hedgehog-installer.iso?token=xxx"
iso_checksum: "sha256:d88bf389cbbb255f4f189709861b55c391dde5e1173da3c4c5c6d2ac8c954a48"
mgmt_nic_pci: "pci_0000_58_00_0"
bridge_name: "virbr-libvirt"
vm_mac: "02:7f:3c:a9:4d:e2"
autostart: true