Developer Guide — Packages
This page documents the internal package structure of pipam for developers
working on the codebase. The source lives in code/proxmox-ipam/.
Package map
Section titled “Package map”code/proxmox-ipam/ cmd/pipam/ CLI entrypoint config/ Configuration types and loading proxmox/ Proxmox API client proxmox/auth/ Authentication strategies state/ State model, collection, diffing cluster/ Raft consensus and persistence dns/ DNS provider interface dns/powerdns/ PowerDNS implementation ipam/ IPAM provider interface ipam/netbox/ NetBox implementation sync/ Reconcilercmd/pipam/ — CLI entrypoint
Section titled “cmd/pipam/ — CLI entrypoint”Uses cobra for commands and viper for config/env binding.
| File | Description |
|---|---|
main.go | Root command, viper init, config file discovery |
run.go | pipam run — daemon mode (single-node or raft) |
output.go | pipam output — one-shot collection and pretty/JSON output |
reconcile.go | pipam reconcile — one-shot full reconcile |
clean.go | pipam clean — remove all managed DNS/IPAM entries |
health.go | pipam health — check provider connectivity |
providers.go | Factory functions for DNS and IPAM providers from config |
Daemon loop (run.go)
Section titled “Daemon loop (run.go)”Two modes based on raft.node_id:
- Single-node (
runSingleNodeLoop): timer-based collect → diff → reconcile. State is kept in memory (lastReconciled *state.Snapshot). - Cluster (
runMainLoop): leader collects and proposes snapshots via raft. Reconciliation triggers onApplied()channel when raft commits a new snapshot. Reconciled state is proposed back through raft for replication.
Both loops support full_reconcile_interval — periodically diffs against nil
so all current entries are re-ensured.
config/ — Configuration
Section titled “config/ — Configuration”| File | Description |
|---|---|
types.go | Config struct hierarchy with mapstructure tags |
parse.go | Viper-based loading, secret resolution, custom decode hooks |
Key design decisions:
- Viper key delimiter is
::(not.) to avoid conflicts with TOML dotted keys in mapping sections. - Secret resolution order: env var → file → inline config value.
stringToIPNetHookFuncconverts CIDR strings to*net.IPNetduring unmarshalling.MappingConfigkeys are domain names (e.g."services.homelab.muehlena.de"). Each contains a list of networks with CIDRs and optional service records.
Resolution functions
Section titled “Resolution functions”config.ResolveFQDN(mapping, hostname, ip) — longest-prefix-match IP against
all mapping network CIDRs, returns hostname.domain. Returns hostname as-is if
it already contains a dot (FQDN).
config.DomainForIP(mapping, ip) — returns just the matching domain (no
hostname prefix).
config.ResolveServiceRecords(mapping, ip) — returns shared FQDNs for networks
that have service_record configured.
proxmox/ — Proxmox API client
Section titled “proxmox/ — Proxmox API client”| File | Description |
|---|---|
client.go | HTTP client, authentication, JSON response parsing |
nodes.go | Node listing and node-level network interfaces |
vm.go | VM listing, config, guest agent (hostname, interfaces) |
lxc.go | LXC listing, config, interfaces |
version.go | Proxmox version check (used for health) |
Generic request pattern
Section titled “Generic request pattern”All API calls use requestor[T] — a generic helper that deserializes the
Proxmox {"data": T} envelope.
VM vs LXC differences
Section titled “VM vs LXC differences”- VMs: hostname from guest agent (
/agent/get-host-name), interfaces from guest agent (/agent/network-get-interfaces), config for MAC/bridge matching. Guest agent may be unavailable — falls back to VM name. - LXCs: hostname from config (
/config→hostnamefield), interfaces from/interfacesendpoint. IPv6 addresses are in theip-addressesarray (theinet6field only has link-local). Prefix is returned as a string.
proxmox/auth/
Section titled “proxmox/auth/”Authenticator interface with ProxmoxAPITokenAuthenticator implementation.
Sets the Authorization: PVEAPIToken=user@realm!tokenid=uuid header.
state/ — State model
Section titled “state/ — State model”| File | Description |
|---|---|
types.go | Entry, Interface, CIDR, Snapshot types |
pipamcfg.go | PIPAMConfig type and description parser |
collector.go | Builds a Snapshot from Proxmox API data |
diff.go | Diff(old, new) computes added/removed/changed |
Central data type representing a VM or LXC:
type Entry struct { ID string // "{node}/{type}/{vmid}" Node string Type string // "qemu" or "lxc" VMID int Name string // Proxmox display name Hostname string // resolved hostname (agent or config) VCPUs int MemoryMB int DiskMB int Tags []string // Proxmox tags (semicolon-separated, parsed) PIPAMCfg *PIPAMConfig // per-VM config from description Interfaces []Interface}Collector
Section titled “Collector”Collector.Collect(ctx) iterates all Proxmox nodes, fetches running VMs and
LXCs, resolves hostnames, builds interfaces with IP addresses, and applies
filtering:
- Skip non-running VMs/LXCs
- Skip loopback, link-local, multicast IPs
- Skip IPs in
skip_cidrs - Parse
PIPAMConfigfrom description field - Apply
ip_mapremapping - Include entry only if hostname is FQDN, has a mapping override, or at least one IP matches a global mapping
VLAN resolution
Section titled “VLAN resolution”resolveVLAN walks the bridge → bridge_ports → VLAN chain on the Proxmox node
to find the backing VLAN ID for a bridge interface.
Diff(old, new *Snapshot) *DiffResult — compares by entry ID. Uses JSON
serialization for equality. Returns Added, Removed, Changed lists.
A nil old snapshot means everything is added (used for full reconcile). A nil new snapshot means everything is removed (used for clean).
cluster/ — Raft consensus
Section titled “cluster/ — Raft consensus”| File | Description |
|---|---|
raft.go | RaftNode wrapping go.etcd.io/raft/v3 |
fsm.go | Finite state machine (current + reconciled state) |
wal.go | WAL and snapshot persistence to disk |
transport.go | HTTP transport for raft messages between nodes |
RaftNode
Section titled “RaftNode”- Fresh start:
raft.StartNode()with all peers - Restart (WAL exists): replay into
MemoryStorage→raft.RestartNode() - Event loop: tick → Ready → persist WAL → send messages → apply to FSM → Advance
ProposeSnapshot(ctx, snapshot)— proposes collected stateProposeReconciled(ctx, snapshot)— proposes reconciled stateApplied() <-chan struct{}— notifies when new state is committedIsLeader() bool
Holds current, previous, and reconciled snapshots. Two proposal types:
ProposalSnapshot— updates current/previous (triggers reconciliation)ProposalReconciled— updates reconciled state (does not trigger reconciliation)
This distinction prevents tight retry loops: reconciled-state commits don’t re-trigger the reconciler.
File-based persistence in raft.data_dir. Stores raft hard state, log entries,
and FSM state. Enables recovery after full cluster restart.
Transport
Section titled “Transport”HTTP-based: POST /raft/message sends protobuf raftpb.Message between nodes.
Peers are discovered from the raft.peers config map.
dns/ — DNS provider
Section titled “dns/ — DNS provider”DNSProvider interface:
type DNSProvider interface { EnsureRecord(ctx, fqdn, address, rrtype string) error DeleteRecord(ctx, fqdn, address, rrtype string) error Health(ctx) error}dns/powerdns/
Section titled “dns/powerdns/”PowerDNS implementation via REST API. Records are managed per-zone using
PATCH /api/v1/servers/localhost/zones/{zone} with rrsets changetype
REPLACE or DELETE. Zone is resolved from the FQDN by matching against
configured zones.
EnsureRecord merges the new address into existing records for the same
FQDN/rrtype (fetches current records first, appends, PATCHes).
ipam/ — IPAM provider
Section titled “ipam/ — IPAM provider”IPAMProvider interface:
type IPAMProvider interface { EnsureVM(ctx, entry state.Entry) error DeleteVM(ctx, entry state.Entry) error Health(ctx) error}ipam/netbox/
Section titled “ipam/netbox/”NetBox implementation. EnsureVM performs a cascade of idempotent operations:
- Find or create VM (
/api/virtualization/virtual-machines/) - Set VM metadata: vcpus, memory, disk, tags, comments, cluster
- For each interface: find/create interface, assign MAC address via dcim, assign VLAN
- For each IP: resolve prefix from NetBox prefixes, find/create IP address, assign to interface
- Remove stale IPs and interfaces not in the current entry
- Set primary IPv4/IPv6 (single-interface heuristic or
primary_macfrom PIPAM config)
Prefix resolution queries /api/ipam/prefixes/?contains=<ip> and uses the
longest-match prefix, overriding the prefix reported by Proxmox.
Tags: qemu and lxc type tags are created automatically. All other tags
(Proxmox tags, PIPAM config tags) are only included if they already exist in
NetBox.
sync/ — Reconciler
Section titled “sync/ — Reconciler”Reconciler.Reconcile(ctx, diff) applies a DiffResult to DNS and IPAM:
- Added entries: ensure DNS records + IPAM VM
- Changed entries: ensure (idempotent update)
- Removed entries: delete DNS records + IPAM VM
Returns ReconcileResult with FailedIDs — entries that failed are excluded
from the reconciled state so they are retried on the next cycle.
FQDN resolution with overrides
Section titled “FQDN resolution with overrides”The reconciler checks PIPAMConfig.Mapping (per-network) before falling back to
the global mapping config. This allows per-VM domain overrides.
IPAM filtering
Section titled “IPAM filtering”filterEntryForIPAM creates a copy of the entry containing only IPs that
resolve to a domain (global mapping or PIPAM override). FQDN hostnames include
all IPs. IPv6 inheritance applies when the interface has matched IPv4 and
ipv6_inherit_ipv4_fqdn is enabled.
Reconciliation and retry flow
Section titled “Reconciliation and retry flow”flowchart LR Collect --> Propose --> Commit Commit --> Diff
subgraph Reconcile Diff --> Added Diff --> Changed Diff --> Removed Added -->|success| RS[Reconciled State] Added -->|failure| Excluded[Excluded from RS] Changed -->|success| RS Changed -->|failure| Reverted[Reverted in RS] Removed -->|success| RS Removed -->|failure| Kept[Kept in RS] end
RS --> ProposeRS[Propose reconciled] ProposeRS --> CommitRS[Commit via raft]
Excluded -.->|next cycle| Diff Reverted -.->|next cycle| Diff Kept -.->|next cycle| DiffAdding a new provider
Section titled “Adding a new provider”- Create
dns/<name>/client.goimplementingdns.DNSProvider - Add config type to
config/types.gounderDNSConfig - Add case to
makeDNSProviderincmd/pipam/providers.go
- Create
ipam/<name>/client.goimplementingipam.IPAMProvider - Add config type to
config/types.gounderIPAMConfig - Add case to
makeIPAMProviderincmd/pipam/providers.go