From 08472ff950230fcdb7fc993dcaec5af57f885d6a Mon Sep 17 00:00:00 2001 From: BenVincent Date: Thu, 6 Aug 2026 22:23:51 +1000 Subject: [PATCH 1/6] chore: update dns nameserver in dhcp (#507) - set dhcp to provide 198.18.200.7 as dns in syd1 --------- Co-authored-by: Ben Vincent Reviewed-on: https://git.unkin.net/unkin/puppet-prod/pulls/507 --- hieradata/country/au/region/syd1.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hieradata/country/au/region/syd1.yaml b/hieradata/country/au/region/syd1.yaml index f32bd93..7639c9e 100644 --- a/hieradata/country/au/region/syd1.yaml +++ b/hieradata/country/au/region/syd1.yaml @@ -2,7 +2,7 @@ timezone::timezone: 'Australia/Sydney' certbot::client::webserver: ausyd1nxvm2057.main.unkin.net profiles_dns_upstream_forwarder_unkin: - - 198.18.19.15 + - 198.18.200.7 profiles_dns_upstream_forwarder_consul: - 198.18.19.14 profiles_dns_upstream_forwarder_k8s: From caf27e90bde7a6f9f72c089fb0dad529de63ea2e Mon Sep 17 00:00:00 2001 From: BenVincent Date: Sat, 8 Aug 2026 00:41:10 +1000 Subject: [PATCH 2/6] chore: add new physicals (#510) add prodnxsr0014-19, ocnfigure networking in preparation for ceph --------- Co-authored-by: Ben Vincent Reviewed-on: https://git.unkin.net/unkin/puppet-prod/pulls/510 --- hieradata/nodes/prodnxsr0014.main.unkin.net.yaml | 13 +++++++++++++ hieradata/nodes/prodnxsr0015.main.unkin.net.yaml | 13 +++++++++++++ hieradata/nodes/prodnxsr0016.main.unkin.net.yaml | 13 +++++++++++++ hieradata/nodes/prodnxsr0017.main.unkin.net.yaml | 13 +++++++++++++ hieradata/nodes/prodnxsr0018.main.unkin.net.yaml | 13 +++++++++++++ hieradata/nodes/prodnxsr0019.main.unkin.net.yaml | 13 +++++++++++++ 6 files changed, 78 insertions(+) create mode 100644 hieradata/nodes/prodnxsr0014.main.unkin.net.yaml create mode 100644 hieradata/nodes/prodnxsr0015.main.unkin.net.yaml create mode 100644 hieradata/nodes/prodnxsr0016.main.unkin.net.yaml create mode 100644 hieradata/nodes/prodnxsr0017.main.unkin.net.yaml create mode 100644 hieradata/nodes/prodnxsr0018.main.unkin.net.yaml create mode 100644 hieradata/nodes/prodnxsr0019.main.unkin.net.yaml diff --git a/hieradata/nodes/prodnxsr0014.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0014.main.unkin.net.yaml new file mode 100644 index 0000000..28b1d8e --- /dev/null +++ b/hieradata/nodes/prodnxsr0014.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +networking_loopback0_ip: 198.18.19.14 # management loopback +networking_loopback1_ip: 198.18.22.14 # ceph-cluster loopback +networking_loopback2_ip: 198.18.23.14 # ceph-public loopback +networking_1000_ip: 198.18.15.14 # 1gbe network +networking_2500_ip: 198.18.21.14 # 2.5gbe network +networking_1000_iface: enp1s0 +networking_2500_iface: enp2s0 +networking::interfaces: + "%{hiera('networking_1000_iface')}": + mac: a4:bb:6d:a4:e5:c1 + "%{hiera('networking_2500_iface')}": + mac: c4:62:37:0d:50:03 diff --git a/hieradata/nodes/prodnxsr0015.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0015.main.unkin.net.yaml new file mode 100644 index 0000000..05e1652 --- /dev/null +++ b/hieradata/nodes/prodnxsr0015.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +networking_loopback0_ip: 198.18.19.15 # management loopback +networking_loopback1_ip: 198.18.22.15 # ceph-cluster loopback +networking_loopback2_ip: 198.18.23.15 # ceph-public loopback +networking_1000_ip: 198.18.15.15 # 1gbe network +networking_2500_ip: 198.18.21.15 # 2.5gbe network +networking_1000_iface: enp1s0 +networking_2500_iface: enp2s0 +networking::interfaces: + "%{hiera('networking_1000_iface')}": + mac: a4:bb:6d:a6:30:c4 + "%{hiera('networking_2500_iface')}": + mac: c4:62:37:0d:4f:f4 diff --git a/hieradata/nodes/prodnxsr0016.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0016.main.unkin.net.yaml new file mode 100644 index 0000000..cecf63a --- /dev/null +++ b/hieradata/nodes/prodnxsr0016.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +networking_loopback0_ip: 198.18.19.16 # management loopback +networking_loopback1_ip: 198.18.22.16 # ceph-cluster loopback +networking_loopback2_ip: 198.18.23.16 # ceph-public loopback +networking_1000_ip: 198.18.15.16 # 1gbe network +networking_2500_ip: 198.18.21.16 # 2.5gbe network +networking_1000_iface: enp1s0 +networking_2500_iface: enp2s0 +networking::interfaces: + "%{hiera('networking_1000_iface')}": + mac: a4:bb:6d:9f:22:13 + "%{hiera('networking_2500_iface')}": + mac: c4:62:37:0d:50:0c diff --git a/hieradata/nodes/prodnxsr0017.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0017.main.unkin.net.yaml new file mode 100644 index 0000000..6d75726 --- /dev/null +++ b/hieradata/nodes/prodnxsr0017.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +networking_loopback0_ip: 198.18.19.17 # management loopback +networking_loopback1_ip: 198.18.22.17 # ceph-cluster loopback +networking_loopback2_ip: 198.18.23.17 # ceph-public loopback +networking_1000_ip: 198.18.15.17 # 1gbe network +networking_2500_ip: 198.18.21.17 # 2.5gbe network +networking_1000_iface: enp1s0 +networking_2500_iface: enp2s0 +networking::interfaces: + "%{hiera('networking_1000_iface')}": + mac: 8c:04:ba:9c:b6:08 + "%{hiera('networking_2500_iface')}": + mac: c4:62:37:0d:50:12 diff --git a/hieradata/nodes/prodnxsr0018.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0018.main.unkin.net.yaml new file mode 100644 index 0000000..7732c31 --- /dev/null +++ b/hieradata/nodes/prodnxsr0018.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +networking_loopback0_ip: 198.18.19.18 # management loopback +networking_loopback1_ip: 198.18.22.18 # ceph-cluster loopback +networking_loopback2_ip: 198.18.23.18 # ceph-public loopback +networking_1000_ip: 198.18.15.18 # 1gbe network +networking_2500_ip: 198.18.21.18 # 2.5gbe network +networking_1000_iface: enp1s0 +networking_2500_iface: enp2s0 +networking::interfaces: + "%{hiera('networking_1000_iface')}": + mac: a4:bb:6d:a4:db:94 + "%{hiera('networking_2500_iface')}": + mac: c4:62:37:0d:4f:fa diff --git a/hieradata/nodes/prodnxsr0019.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0019.main.unkin.net.yaml new file mode 100644 index 0000000..76b431b --- /dev/null +++ b/hieradata/nodes/prodnxsr0019.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +networking_loopback0_ip: 198.18.19.19 # management loopback +networking_loopback1_ip: 198.18.22.19 # ceph-cluster loopback +networking_loopback2_ip: 198.18.23.19 # ceph-public loopback +networking_1000_ip: 198.18.15.19 # 1gbe network +networking_2500_ip: 198.18.21.19 # 2.5gbe network +networking_1000_iface: enp1s0 +networking_2500_iface: enp2s0 +networking::interfaces: + "%{hiera('networking_1000_iface')}": + mac: a4:bb:6d:a4:56:11 + "%{hiera('networking_2500_iface')}": + mac: c4:62:37:0d:50:00 From 895ca5b1cbfcf162a9f6bc828cf66c7810e3d17f Mon Sep 17 00:00:00 2001 From: BenVincent Date: Sat, 8 Aug 2026 00:56:17 +1000 Subject: [PATCH 3/6] fix: avoid anycast ips (#511) must change management interfaces for new nodes to start from 21, since .14 to .20 were used for various anycast services really need ipam --------- Co-authored-by: Ben Vincent Reviewed-on: https://git.unkin.net/unkin/puppet-prod/pulls/511 --- hieradata/nodes/prodnxsr0014.main.unkin.net.yaml | 2 +- hieradata/nodes/prodnxsr0015.main.unkin.net.yaml | 2 +- hieradata/nodes/prodnxsr0016.main.unkin.net.yaml | 2 +- hieradata/nodes/prodnxsr0017.main.unkin.net.yaml | 2 +- hieradata/nodes/prodnxsr0018.main.unkin.net.yaml | 2 +- hieradata/nodes/prodnxsr0019.main.unkin.net.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hieradata/nodes/prodnxsr0014.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0014.main.unkin.net.yaml index 28b1d8e..6c6f6b0 100644 --- a/hieradata/nodes/prodnxsr0014.main.unkin.net.yaml +++ b/hieradata/nodes/prodnxsr0014.main.unkin.net.yaml @@ -1,5 +1,5 @@ --- -networking_loopback0_ip: 198.18.19.14 # management loopback +networking_loopback0_ip: 198.18.19.21 # management loopback networking_loopback1_ip: 198.18.22.14 # ceph-cluster loopback networking_loopback2_ip: 198.18.23.14 # ceph-public loopback networking_1000_ip: 198.18.15.14 # 1gbe network diff --git a/hieradata/nodes/prodnxsr0015.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0015.main.unkin.net.yaml index 05e1652..47cb924 100644 --- a/hieradata/nodes/prodnxsr0015.main.unkin.net.yaml +++ b/hieradata/nodes/prodnxsr0015.main.unkin.net.yaml @@ -1,5 +1,5 @@ --- -networking_loopback0_ip: 198.18.19.15 # management loopback +networking_loopback0_ip: 198.18.19.22 # management loopback networking_loopback1_ip: 198.18.22.15 # ceph-cluster loopback networking_loopback2_ip: 198.18.23.15 # ceph-public loopback networking_1000_ip: 198.18.15.15 # 1gbe network diff --git a/hieradata/nodes/prodnxsr0016.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0016.main.unkin.net.yaml index cecf63a..5a9445b 100644 --- a/hieradata/nodes/prodnxsr0016.main.unkin.net.yaml +++ b/hieradata/nodes/prodnxsr0016.main.unkin.net.yaml @@ -1,5 +1,5 @@ --- -networking_loopback0_ip: 198.18.19.16 # management loopback +networking_loopback0_ip: 198.18.19.23 # management loopback networking_loopback1_ip: 198.18.22.16 # ceph-cluster loopback networking_loopback2_ip: 198.18.23.16 # ceph-public loopback networking_1000_ip: 198.18.15.16 # 1gbe network diff --git a/hieradata/nodes/prodnxsr0017.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0017.main.unkin.net.yaml index 6d75726..1ed6465 100644 --- a/hieradata/nodes/prodnxsr0017.main.unkin.net.yaml +++ b/hieradata/nodes/prodnxsr0017.main.unkin.net.yaml @@ -1,5 +1,5 @@ --- -networking_loopback0_ip: 198.18.19.17 # management loopback +networking_loopback0_ip: 198.18.19.24 # management loopback networking_loopback1_ip: 198.18.22.17 # ceph-cluster loopback networking_loopback2_ip: 198.18.23.17 # ceph-public loopback networking_1000_ip: 198.18.15.17 # 1gbe network diff --git a/hieradata/nodes/prodnxsr0018.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0018.main.unkin.net.yaml index 7732c31..6f2d9eb 100644 --- a/hieradata/nodes/prodnxsr0018.main.unkin.net.yaml +++ b/hieradata/nodes/prodnxsr0018.main.unkin.net.yaml @@ -1,5 +1,5 @@ --- -networking_loopback0_ip: 198.18.19.18 # management loopback +networking_loopback0_ip: 198.18.19.25 # management loopback networking_loopback1_ip: 198.18.22.18 # ceph-cluster loopback networking_loopback2_ip: 198.18.23.18 # ceph-public loopback networking_1000_ip: 198.18.15.18 # 1gbe network diff --git a/hieradata/nodes/prodnxsr0019.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0019.main.unkin.net.yaml index 76b431b..7ee1ae4 100644 --- a/hieradata/nodes/prodnxsr0019.main.unkin.net.yaml +++ b/hieradata/nodes/prodnxsr0019.main.unkin.net.yaml @@ -1,5 +1,5 @@ --- -networking_loopback0_ip: 198.18.19.19 # management loopback +networking_loopback0_ip: 198.18.19.26 # management loopback networking_loopback1_ip: 198.18.22.19 # ceph-cluster loopback networking_loopback2_ip: 198.18.23.19 # ceph-public loopback networking_1000_ip: 198.18.15.19 # 1gbe network From 10854b65012fd4a9aa9733b8a6025d3b3ca862ee Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sat, 8 Aug 2026 18:18:55 +1000 Subject: [PATCH 4/6] fix: preload correct rke2 airgap images to break CNI bootstrap deadlock (#512) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Four newly-provisioned el9_8 compute nodes (prodnxsr0014/0015/0018/0019) hang with canal's kube-flannel container in `ImagePullBackOff`: the flannel VXLAN overlay never comes up, so the node can't reach any in-cluster `198.18.200.x` LoadBalancer VIP. Root cause is a stale airgap-preload version. The nodes boot rke2 **v1.33.11+rke2r1** from the rolling `latest/1.33` repo, whose canal manifest requests `hardened-flannel:v0.28.4` / `hardened-calico:v3.31.5` (build20260415). But `rke2::install` pinned the preloaded bundle to **v1.33.4+rke2r1** (flannel v0.27.2 / calico v3.30.2), so those images were never on disk. containerd then falls back to the `docker.io` mirror (artifactapi, `disable-default-registry-endpoint: true`), reachable only via the pod-overlay VIP that requires the very flannel image being pulled — a bootstrap deadlock. Older nodes (0001-0008) are versionlocked at 1.33.4 and still match their original bundle, so they were unaffected. ## Changes - Bump pinned `rke2_version` `1.33.4` -> `1.33.11` so the versionlock, RPM `ensure`, and preloaded bundle all line up with the canal image tags the running binary requests. The default `rke2-images.linux-amd64.tar.zst` bundle already contains the canal CNI images (it is RKE2's default CNI), so no extra tarball is needed. - Wire the airgap archive `source` to the `container_archive_source` class parameter (previously declared in the module but never consumed). The module keeps its generic upstream default; the artifactapi override (the pre-CNI-reachable source, same BGP/physical path the rke2 yum repos already use) lives in the k8s role hiera as `rke2::container_archive_source`. Applies to servers and agents alike (`rke2::install` runs for both) and preloads `before => Service`, so the bundle lands before rke2 starts. Notes: - The `latest/1.33` repo is rolling, so the pinned version must be maintained as the repo advances; a follow-up to pin the yum channel to a fixed patch would remove the drift entirely. - No terraform-artifactapi change is required. (If a canal-only preload were ever wanted, the github generic remote allowlist would need `rancher/rke2/.*/rke2-images-canal.linux-amd64.tar.zst$` added — but the default bundle already carries those images, so it is unnecessary.) https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT Reviewed-on: https://git.unkin.net/unkin/puppet-prod/pulls/512 Co-authored-by: Ben Vincent Co-committed-by: Ben Vincent --- hieradata/roles/infra/k8s.yaml | 2 ++ modules/rke2/manifests/install.pp | 4 ++-- modules/rke2/manifests/params.pp | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/hieradata/roles/infra/k8s.yaml b/hieradata/roles/infra/k8s.yaml index d8c9bcb..b91b86a 100644 --- a/hieradata/roles/infra/k8s.yaml +++ b/hieradata/roles/infra/k8s.yaml @@ -11,6 +11,8 @@ hiera_include: # manage rke2 rke2::bootstrap_node: prodnxsr0001.main.unkin.net rke2::join_url: https://join-k8s.service.consul:9345 +# pull the airgap image bundle via artifactapi (host-reachable pre-CNI), not github directly +rke2::container_archive_source: 'https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/github/rancher/rke2/releases/download' rke2::manage_registries: true rke2::registries: docker.io: diff --git a/modules/rke2/manifests/install.pp b/modules/rke2/manifests/install.pp index f15764d..7c5b7b3 100644 --- a/modules/rke2/manifests/install.pp +++ b/modules/rke2/manifests/install.pp @@ -27,10 +27,10 @@ class rke2::install ( before => Service["rke2-${node_type}"], } - # download required archive of containers + # preload the airgap bundle (has the default canal CNI images) so canal starts from disk, not the mirror VIP that needs flannel first archive { '/var/lib/rancher/rke2/agent/images/rke2-images.linux-amd64.tar.zst': ensure => present, - source => "https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/github/rancher/rke2/releases/download/v${rke2_version}%2B${rke2_release}/rke2-images.linux-amd64.tar.zst", + source => "${container_archive_source}/v${rke2_version}%2B${rke2_release}/rke2-images.linux-amd64.tar.zst", require => [ Package["rke2-${node_type}"], File['/var/lib/rancher/rke2/agent/images'], diff --git a/modules/rke2/manifests/params.pp b/modules/rke2/manifests/params.pp index 4c110dc..9d5a443 100644 --- a/modules/rke2/manifests/params.pp +++ b/modules/rke2/manifests/params.pp @@ -1,7 +1,7 @@ # rke2 params class rke2::params ( Enum['server', 'agent'] $node_type = 'agent', - String $rke2_version = '1.33.4', + String $rke2_version = '1.33.11', String $rke2_release = 'rke2r1', Stdlib::Absolutepath $config_file = '/etc/rancher/rke2/config.yaml', Hash $config_hash = {}, From 499251575d1b2a27c4b32c07835dcc7c216030bc Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sat, 8 Aug 2026 18:41:50 +1000 Subject: [PATCH 5/6] lldpd: run on physicals and expose neighbour topology as the lldp fact (#513) ## Why LLDP is the only source of physical switch/port topology in the estate: which switch and which port each machine is cabled to exists nowhere else (not in intent YAML, not in any other fact, not in DHCP/DNS). NetBox needs it to record device interface -> switch/port connections. This installs and enables lldpd on physical hosts and exposes its neighbour data as a structured `lldp` fact for the terraform-infra pdbmux backfill to consume (linked issue below). ## Changes - Add `profiles::lldpd`: install the `lldpd` package and enable+start the service (it ships disabled), the service subscribing to the package. - Assign it physicals-only via `hiera_include` in `hieradata/virtual/physical.yaml` (`facts.virtual == 'physical'`), merged `unique` with the common `hiera_include`. VMs never receive it. - Add the `lldp` custom fact (`modules/libs/lib/facter/lldp.rb`): parse `lldpctl -f json0` into a per-interface map of `{neighbor_chassis_name, neighbor_chassis_mac, neighbor_chassis_descr, neighbor_port_id, neighbor_port_descr, vlan_id, vlan_name}`, skipping interfaces with no neighbour. Confined to physical Linux hosts that have `lldpctl` and a live lldpd socket; returns an empty hash on any error so it can never break a puppet run. json0 is used because it array-wraps every node regardless of cardinality, so one neighbour and many neighbours parse identically (plain `keyvalue` folds the neighbour SysName into the key path; plain `json` collapses single-element arrays into objects). ## Sample fact output (prodnxsr0019, enp1s0) ```json { "enp1s0": { "neighbor_chassis_name": "sg3429x-m2-02", "neighbor_chassis_mac": "b8:fb:b3:a7:f9:5d", "neighbor_chassis_descr": "48-Port Gigabit Smart Managed Pro Switch", "neighbor_port_id": "24", "neighbor_port_descr": "two-gigabitEthernet 1/0/11", "vlan_id": "201", "vlan_name": "fabric_common_02" } } ``` ## Consumer NetBox population of the switch/port cabling is tracked in terraform-infra: https://git.unkin.net/unkin/terraform-infra/issues/7 (extend the pdbmux backfill / PR #6 to read this fact and emit the connection). https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT Reviewed-on: https://git.unkin.net/unkin/puppet-prod/pulls/513 Co-authored-by: Ben Vincent Co-committed-by: Ben Vincent --- hieradata/virtual/physical.yaml | 5 ++ modules/libs/lib/facter/lldp.rb | 104 +++++++++++++++++++++++++++++++ site/profiles/manifests/lldpd.pp | 33 ++++++++++ 3 files changed, 142 insertions(+) create mode 100644 modules/libs/lib/facter/lldp.rb create mode 100644 site/profiles/manifests/lldpd.pp diff --git a/hieradata/virtual/physical.yaml b/hieradata/virtual/physical.yaml index edee18f..d021242 100644 --- a/hieradata/virtual/physical.yaml +++ b/hieradata/virtual/physical.yaml @@ -1,3 +1,8 @@ --- +# physical hosts only (facts.virtual == 'physical'); merged 'unique' with the +# common hiera_include in profiles::base. +hiera_include: + - profiles::lldpd + profiles::packages::include: "%{hiera('lm-sensors::package')}": {} diff --git a/modules/libs/lib/facter/lldp.rb b/modules/libs/lib/facter/lldp.rb new file mode 100644 index 0000000..283630b --- /dev/null +++ b/modules/libs/lib/facter/lldp.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'facter' +require 'json' + +# Exposes LLDP neighbour topology (switch/port each interface is cabled to) as +# the structured `lldp` fact, keyed by local interface. This is the only source +# of physical switch/port topology in the estate and feeds NetBox. Uses +# `lldpctl -f json0`: json0 wraps every node in an array regardless of +# cardinality, so one neighbour and many neighbours parse identically (plain +# `keyvalue` folds the neighbour's sysname into the key path, and plain `json` +# collapses single-element arrays into objects). Never raises: any error or a +# down daemon yields an empty hash so a puppet run can never break. +module LldpFact + SOCKETS = ['/run/lldpd.socket', '/var/run/lldpd.socket'].freeze + + module_function + + # First element of a json0 node (everything is array-wrapped), or the value + # itself if it is not an array; nil when absent. + def first(node) + node.is_a?(Array) ? node[0] : node + end + + # Array form of a json0 node whatever its cardinality. + def list(node) + node.is_a?(Array) ? node : [node].compact + end + + # Value string of a json0 leaf like [{ 'value' => 'x' }]. + def leaf(node) + entry = first(node) + entry.is_a?(Hash) ? entry['value'] : entry + end + + # Chassis MAC from its id list, preferring the entry typed 'mac'. + def chassis_mac(chassis) + ids = list(chassis['id']) + mac = ids.find { |id| id.is_a?(Hash) && id['type'] == 'mac' } || ids.first + mac.is_a?(Hash) ? mac['value'] : nil + end + + # Topology record for one local interface, or nil when it has no neighbour. + def neighbour(iface) + chassis = first(iface['chassis']) + port = first(iface['port']) + return nil unless chassis && port + + chassis_fields(chassis).merge(port_fields(port, first(iface['vlan']))) + end + + def chassis_fields(chassis) + { + 'neighbor_chassis_name' => leaf(chassis['name']), + 'neighbor_chassis_mac' => chassis_mac(chassis), + 'neighbor_chassis_descr' => leaf(chassis['descr']) + } + end + + def port_fields(port, vlan) + port_id = first(port['id']) + vlan_h = vlan.is_a?(Hash) ? vlan : {} + { + 'neighbor_port_id' => port_id.is_a?(Hash) ? port_id['value'] : port_id, + 'neighbor_port_descr' => leaf(port['descr']), + 'vlan_id' => vlan_h['vlan-id'], + 'vlan_name' => vlan_h['value'] + } + end + + def interfaces(output) + lldp = first(JSON.parse(output)['lldp']) || {} + list(lldp['interface']) + end + + # Map of local interface => topology record, skipping interfaces with no + # neighbour. + def collect(ifaces) + ifaces.each_with_object({}) do |iface, acc| + next unless iface.is_a?(Hash) + + name = iface['name'] + data = neighbour(iface) + acc[name] = data if name && data + end + end + + def resolve + output = Facter::Core::Execution.execute('lldpctl -f json0 2>/dev/null', on_fail: nil) + return {} if output.to_s.empty? + + collect(interfaces(output)) + rescue StandardError + {} + end +end + +Facter.add(:lldp) do + confine kernel: 'Linux' + confine { Facter.value(:is_virtual) == false } + confine { Facter::Core::Execution.which('lldpctl') } + confine { LldpFact::SOCKETS.any? { |path| File.exist?(path) } } + setcode { LldpFact.resolve } +end diff --git a/site/profiles/manifests/lldpd.pp b/site/profiles/manifests/lldpd.pp new file mode 100644 index 0000000..ce025af --- /dev/null +++ b/site/profiles/manifests/lldpd.pp @@ -0,0 +1,33 @@ +# profiles::lldpd +# +# Runs lldpd on physical hosts so each machine learns its switch/port topology +# via LLDP. The `lldp` fact exposes that neighbour data for NetBox. Assigned +# via hiera_include from hieradata/virtual/physical.yaml (physicals only); the +# lldpd.service ships disabled, so it is explicitly enabled and started here. +class profiles::lldpd ( + Boolean $enabled = true, + String $package = 'lldpd', + String $service = 'lldpd', +){ + + if $enabled { + package { $package: + ensure => installed, + } + + service { $service: + ensure => running, + enable => true, + subscribe => Package[$package], + } + } else { + service { $service: + ensure => stopped, + enable => false, + } + + package { $package: + ensure => absent, + } + } +} From 0fdc7c97d8478a657157d2840ee26ef79fb3459a Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sat, 8 Aug 2026 20:00:42 +1000 Subject: [PATCH 6/6] rke2,puppet: enforce versionlock before package install (#514) ## Why A package versionlock is only effective if it is applied **before** the package resource is evaluated. When the package is upgraded/installed first and locked afterwards, dnf can pull an unpinned version and the lock then pins whatever landed. This is the exact drift that let new RKE2 nodes boot 1.33.11 instead of the pinned version, and it also matters because a global `dnf update -y` autoupdate timer (`profiles::yum::autoupdater`) relies on the versionlock plugin already having the lock written. ## Changes - `rke2::install` orders the versionlock ahead of the package: adds `before => Package["rke2-${node_type}"]` so the lock exists before install/upgrade. - `profiles::puppet::agent` (RedHat branch) adds `before => Package[$use_package]` on the versionlock for the same reason. - Leaves `profiles::gitea::runner` and `profiles::vault::server` unchanged: both already order the versionlock ahead of the package (`before => Package` / `before => Class['vault']`). - Changes ordering only; locked versions (including the RKE2 pin) are untouched. No dependency cycle is introduced (packages hold no back-reference to the versionlock). https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT Reviewed-on: https://git.unkin.net/unkin/puppet-prod/pulls/514 Co-authored-by: Ben Vincent Co-committed-by: Ben Vincent --- modules/rke2/manifests/install.pp | 3 ++- site/profiles/manifests/puppet/agent.pp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/rke2/manifests/install.pp b/modules/rke2/manifests/install.pp index 7c5b7b3..4630f3c 100644 --- a/modules/rke2/manifests/install.pp +++ b/modules/rke2/manifests/install.pp @@ -6,10 +6,11 @@ class rke2::install ( Stdlib::HTTPUrl $container_archive_source = $rke2::container_archive_source, ){ - # versionlock rke2 + # versionlock rke2 before install so the lock exists before any upgrade is attempted yum::versionlock{"rke2-${node_type}": ensure => present, version => "${rke2_version}~${rke2_release}", + before => Package["rke2-${node_type}"], } # install rke2 diff --git a/site/profiles/manifests/puppet/agent.pp b/site/profiles/manifests/puppet/agent.pp index b8847c2..35d365c 100644 --- a/site/profiles/manifests/puppet/agent.pp +++ b/site/profiles/manifests/puppet/agent.pp @@ -60,10 +60,11 @@ class profiles::puppet::agent ( require => Yumrepo[$use_yumrepo], } - # versionlock puppet-agent + # versionlock puppet-agent before install so the lock exists before any upgrade is attempted yum::versionlock{$use_package: ensure => $agent_versionlock_ensure, version => $agent_versionlock_version, + before => Package[$use_package], } } 'Debian': {