Compare commits

..

4 Commits

Author SHA1 Message Date
unkinben c39caeb8bb fix: preload correct rke2 airgap images to break CNI bootstrap deadlock
ci/woodpecker/pr/ruby-validate Pipeline was successful
ci/woodpecker/pr/puppet-lint Pipeline was successful
ci/woodpecker/pr/yamllint Pipeline was successful
ci/woodpecker/pr/bolt-validate Pipeline was successful
ci/woodpecker/pr/erb-validate Pipeline was successful
ci/woodpecker/pr/epp-validate Pipeline was successful
ci/woodpecker/pr/ruby-check Pipeline was successful
ci/woodpecker/pr/puppet-validate Pipeline was canceled
New el9_8 nodes boot rke2 v1.33.11 (flannel v0.28.4 / calico v3.31.5) from
the rolling latest/1.33 repo, but the airgap preload pinned v1.33.4
(flannel v0.27.2 / calico v3.30.2), so canal's images were never on disk and
containerd fell back to the artifactapi mirror VIP, which is unreachable until
the flannel overlay it would provide is up. Classic bootstrap deadlock.

- bump pinned rke2_version 1.33.4 -> 1.33.11 so the versionlock, RPM and
  preloaded bundle all match the canal image tags the running binary requests
  (the default rke2-images bundle already contains the canal CNI images)
- wire the airgap archive source to the container_archive_source parameter
  (previously declared but unused) and refresh its default to the artifactapi
  github remote, so the pre-CNI-reachable source is hiera-overridable per node

Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
2026-08-08 18:11:27 +10:00
benvin 895ca5b1cb 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 <ben@unkin.net>
Reviewed-on: #511
2026-08-08 00:56:17 +10:00
benvin caf27e90bd chore: add new physicals (#510)
add prodnxsr0014-19, ocnfigure networking in preparation for ceph

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #510
2026-08-08 00:41:10 +10:00
benvin 08472ff950 chore: update dns nameserver in dhcp (#507)
- set dhcp to provide 198.18.200.7 as dns in syd1

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #507
2026-08-06 22:23:51 +10:00
12 changed files with 83 additions and 184 deletions
+1 -1
View File
@@ -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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
-130
View File
@@ -1,130 +0,0 @@
# frozen_string_literal: true
require 'facter'
require 'net/http'
require 'uri'
require 'json'
require 'fileutils'
require 'time'
# Serves this node's IP/interface data from NetBox as the structured `netbox`
# fact. NetBox is authoritative and static IPs never change, so every
# successful response is cached forever and reused verbatim whenever NetBox is
# unreachable: an outage serves stale-but-correct data and can never fail a
# puppet run. Only a host that was never cached returns nothing.
module NetboxFacts
TOKEN_FILE = '/etc/puppetlabs/netbox.token'
URL_FILE = '/etc/puppetlabs/netbox.url'
CACHE_FILE = '/var/cache/puppet-netbox/facts.json'
DEFAULT_URL = 'https://netbox.k8s.syd1.au.unkin.net'
# Talks to the NetBox REST API for one node and assembles the fact payload.
class Client
OPEN_TIMEOUT = 3
READ_TIMEOUT = 5
PAGE = 500
def initialize
@token = File.read(TOKEN_FILE).strip
@base = File.exist?(URL_FILE) ? File.read(URL_FILE).strip : DEFAULT_URL
@names = [Facter.value(:fqdn), Facter.value(:hostname)].compact.uniq
end
# Live payload, or nil when the node is absent from NetBox. Raises on any
# transport/parse error so the caller can fall back to cache.
def fetch
device, kind, filter, iface_base = locate
return nil unless device
primary = device.dig('primary_ip', 'address')
{
'source' => kind, 'name' => device['name'], 'primary_ip' => primary,
'interfaces' => interfaces(iface_base, filter, primary),
'fetched_at' => Time.now.utc.iso8601, 'cached' => false
}
end
private
def locate
device = results("/api/dcim/devices/?#{name_query}").first
return [device, 'device', "device_id=#{device['id']}", '/api/dcim/interfaces/'] if device
vm = results("/api/virtualization/virtual-machines/?#{name_query}").first
vm && [vm, 'vm', "virtual_machine_id=#{vm['id']}", '/api/virtualization/interfaces/']
end
def name_query
@names.map { |name| "name=#{URI.encode_www_form_component(name)}" }.join('&')
end
def interfaces(iface_base, filter, primary)
ips = ips_by_interface(filter)
results("#{iface_base}?#{filter}&limit=#{PAGE}").map do |iface|
addrs = ips[iface['id']] || []
mac = iface['mac_address'] || iface.dig('primary_mac_address', 'mac_address')
{ 'name' => iface['name'], 'mac' => mac, 'ips' => addrs, 'primary' => addrs.include?(primary) }
end
end
def ips_by_interface(filter)
results("/api/ipam/ip-addresses/?#{filter}&limit=#{PAGE}").each_with_object({}) do |ip, map|
(map[ip['assigned_object_id']] ||= []) << ip['address']
end
end
def results(path)
JSON.parse(get(URI("#{@base}#{path}")).body).fetch('results', [])
end
def get(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.open_timeout = OPEN_TIMEOUT
http.read_timeout = READ_TIMEOUT
response = http.request(request_for(uri))
return response if response.is_a?(Net::HTTPSuccess)
raise "netbox #{uri.path} -> HTTP #{response.code}"
end
def request_for(uri)
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Token #{@token}"
request['Accept'] = 'application/json'
request
end
end
module_function
def write_cache(data)
FileUtils.mkdir_p(File.dirname(CACHE_FILE))
File.write(CACHE_FILE, JSON.pretty_generate(data))
File.chmod(0o600, CACHE_FILE)
end
def read_cache
return nil unless File.exist?(CACHE_FILE)
JSON.parse(File.read(CACHE_FILE)).merge('cached' => true)
rescue StandardError
nil
end
def resolve
data = Client.new.fetch
return read_cache unless data
write_cache(data)
data
rescue StandardError => e
Facter.warn("netbox fact: live fetch failed (#{e.message}); serving cache")
read_cache
end
end
Facter.add(:netbox) do
confine { File.exist?(NetboxFacts::TOKEN_FILE) }
setcode { NetboxFacts.resolve }
end
+2 -2
View File
@@ -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'],
+2 -2
View File
@@ -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 = {},
@@ -11,7 +11,7 @@ class rke2::params (
Boolean $helm_install = false,
Hash $helm_repos = {},
Array[String[1]] $extra_config_files = [],
Stdlib::HTTPUrl $container_archive_source = 'https://github.com/rancher/rke2/releases/download',
Stdlib::HTTPUrl $container_archive_source = 'https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/github/rancher/rke2/releases/download',
Boolean $manage_registries = false,
Hash $registries = {},
) {}
-1
View File
@@ -35,7 +35,6 @@ class profiles::base () {
include profiles::ssh::service
include profiles::cloudinit::init
include profiles::helpers::node_lookup
include profiles::netbox::facts
include profiles::consul::client
include victorialogs::client::journald
-48
View File
@@ -1,48 +0,0 @@
# profiles::netbox::facts
#
# Seeds the credentials the `netbox` custom fact needs to read this node's
# IP/interface data from NetBox. Inert until $api_token is set: with no token
# the fact is confined off (unenrolled hosts no-op).
#
# NetBox is authoritative and static IPs never change, so the fact caches every
# successful response under $cache_dir forever and reuses it during a NetBox
# outage - an outage can never fail a puppet run.
class profiles::netbox::facts (
Optional[Sensitive[String]] $api_token = undef,
Stdlib::HTTPSUrl $url = 'https://netbox.k8s.syd1.au.unkin.net',
Stdlib::AbsolutePath $token_file = '/etc/puppetlabs/netbox.token',
Stdlib::AbsolutePath $url_file = '/etc/puppetlabs/netbox.url',
Stdlib::AbsolutePath $cache_dir = '/var/cache/puppet-netbox',
) {
if $api_token =~ Undef {
notify { 'netbox-facts-inert':
message => 'profiles::netbox::facts: api_token unset; netbox fact disabled on this host.',
loglevel => 'info',
}
} else {
file { $token_file:
ensure => file,
owner => 'root',
group => 'root',
mode => '0600',
show_diff => false,
content => Sensitive("${api_token.unwrap}\n"),
}
file { $url_file:
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => "${url}\n",
}
file { $cache_dir:
ensure => directory,
owner => 'root',
group => 'root',
mode => '0700',
}
}
}