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: diff --git a/hieradata/nodes/prodnxsr0014.main.unkin.net.yaml b/hieradata/nodes/prodnxsr0014.main.unkin.net.yaml new file mode 100644 index 0000000..6c6f6b0 --- /dev/null +++ b/hieradata/nodes/prodnxsr0014.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +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 +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..47cb924 --- /dev/null +++ b/hieradata/nodes/prodnxsr0015.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +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 +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..5a9445b --- /dev/null +++ b/hieradata/nodes/prodnxsr0016.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +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 +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..1ed6465 --- /dev/null +++ b/hieradata/nodes/prodnxsr0017.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +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 +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..6f2d9eb --- /dev/null +++ b/hieradata/nodes/prodnxsr0018.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +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 +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..7ee1ae4 --- /dev/null +++ b/hieradata/nodes/prodnxsr0019.main.unkin.net.yaml @@ -0,0 +1,13 @@ +--- +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 +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 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/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/modules/rke2/manifests/install.pp b/modules/rke2/manifests/install.pp index f15764d..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 @@ -27,10 +28,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 = {}, 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, + } + } +} 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': {