Compare commits

..

1 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
51 changed files with 400 additions and 875 deletions
+31 -8
View File
@@ -88,13 +88,36 @@
# SSH Hostkey Signing
The `sshca` ssh engine, its `signhost` role, the `sshca/sign/signhost` policy and
the `sshsigner` approle are managed in terraform-vault:
## create ssh engine, key, set ttl
vault secrets enable -path=ssh-host-signer ssh
vault write ssh-host-signer/config/ca generate_signing_key=true
vault secrets tune -max-lease-ttl=87600h ssh-host-signer
- `config/ssh_secret_backend/sshca.yaml`
- `config/ssh_secret_backend_role/sshca/signhost.yaml`
- `config/auth_approle_role/approle/sshsigner.yaml`
- `policies/sshca/sign/signhost.yaml`
## create role
vault write ssh-host-signer/roles/hostrole \
key_type=ca \
algorithm_signer=rsa-sha2-256 \
ttl=87600h \
allow_host_certificates=true \
allowed_domains="unkin.net" \
allow_subdomains=true \
allow_baredomains=true
## get the sshsigner approle id
vault read -field=role_id auth/approle/role/sshsigner/role-id
## create policy to use hostrole
cat <<EOF > sshsign-host.hcl
path "ssh-host-signer/sign/hostrole" {
capabilities = ["create", "update"]
}
EOF
vault policy write sshsign-host-policy sshsign-host.hcl
vault write auth/approle/role/sshsign-host-role \
bind_secret_id=false \
token_policies="sshsign-host-policy" \
token_ttl=30s \
token_max_ttl=30s \
token_bound_cidrs="198.18.17.3/32,198.18.13.32/32,198.18.13.33/32,198.18.13.34/32"
## get the sshsign-host-role approle id
vault read -field=role_id auth/approle/role/sshsign-host-role/role-id
+1 -43
View File
@@ -178,8 +178,6 @@ lookup_options:
convert_to: Sensitive
stalwart::fallback_admin_password:
convert_to: Sensitive
wireguard::interfaces:
convert_to: Sensitive
facts_path: '/opt/puppetlabs/facter/facts.d'
@@ -369,7 +367,6 @@ ssh::server::options:
profiles::ssh::knownhosts::lines:
- '@cert-authority * ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC1HD97vYxLTniE4qNpGuftUlvmkEXIuX8+7nbENv/IzsGUghEDRtyThjQ7ojNKIsQ7f8wXr0gMcI+fAPfrbcOMHCAoYMomikwL0b3h95SZI40q3CyM+0DMnwiVVDX6C1QxkO2Rv9cszSkCa85NotJhXiUuTBI9BFcRPy+mAhbpAru+bfypYofI0wW97XNTl8Jgwmni5MgutBIQAokFIn5ux8iWxndCH3AqDtmkwC5DfQeQ+wZx7rkwqJEpJffQzrjb1gIM6P9hDCVBBVPh/3o80IJ69rFWrJAZUb+JpG4cXJH0NcSW+wqc3JCT/x3q8VlHwOTXSlNNKtOJCRx73mB8e1XTTy2a9FgpKDDg5XQXWHAViJDz1RTRL9gRefMylRgKz4bXoTuY9kJWM8hPTyUejtukbJThlBJc3OmDxBZBF7F0iqB11pHexok43OCEiANodVa36eWu9/5X032Vm48fZ1/akDPY/NSy3wAn7kwut+A0/JAHFHASrq+1mt9YurkJegI+YHXO6eEWpBIpmI7ORHJbGL4MhkHrxYzVamuP8CkU7tXzsv138+wpOcRHNp9yJY4PT40BZkRf/O3O+jt3pj9Dj8rvgywF2W6hFzywh3Y78upOprRkQlQtHfsI8EyrYI8/hUw2u3H+3yPXh3YjWfqvWVG1BRLRHBV7m90uaw=='
- '@cert-authority * ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDi80G0GtKMdRj4azPwxbJW46NG0y8seSVSnvFm2Ka/nckdUb/3lRlQuPYf1tkbqqFlcXjDM8+u0tzM2kLsgfn0Dpujm1fuoA66yGGweBjtxLFErH5+6+/KND5I9w8LMY2AtVztnBk/CVx8RwgrooABDRZiBH7OOAOpJqqFI7LvEsv61zC0nAqbYJ8uxfx+r4lJ9dUhK1woitEqC4npSRUn5KJK+KAEd7AzcUkZb6TO9A3oRPz1nQ/qU+QNMmVUi+wRj9kJR18mxErugyLLTNcFDBqSdHNun3eUNBUmoS2cAa8ZVscOLzbUGejVK9UY06Q7wu+J34Fzl8CN5tBqRHGZ3ykqGZ6gG+O0Egr9Rm3Obgkujpio2uTh27LhBy72vjgJ8kTFmPlzANTJFpKlsy91usSFPh8WZeIo6VpPLqnC32rjfNkfqHyPAeJ3+rdOkUZoDjMoZc3wxxLZTgMU5ud1w4LxQNRkNiaT/tRRNQF8o+pygkS7xxKduBBMzYdRS1OifaS4gyvP82oOyquWywhyFNtG7ph8FQwc34puM7FyxfaJ0XL/+zAfPMhDoOojvofADJo73R+FgVyer+mCJw4KtWJ4JzZrNTYz1Xl8WlhF8RDzOYfSH46qzJFa9FcRqgP4LUkgzdvcQ+1hBFpvpHtw3vl3DiqtZDDbK9SyrEtdnw=='
profiles::base::groups::local:
admins:
@@ -403,7 +400,7 @@ networking::route_defaults:
# logging:
victorialogs::client::journald::enable: true
victorialogs::client::journald::inserturl: https://logs-ingest.k8s.syd1.au.unkin.net:443/insert/journald
victorialogs::client::journald::inserturl: https://vlinsert.service.consul:9428/insert/journald
# FIXME these are for the proxmox ceph cluster
profiles::ceph::client::fsid: 7f7f00cb-95de-498c-8dcc-14b54e4e9ca8
@@ -412,45 +409,6 @@ profiles::ceph::client::mons:
- 10.18.15.2
- 10.18.15.3
# de96a98f cluster topology (prodnxsr0001-0019) - single source of truth for
# /etc/ceph/ceph.conf rendered by profiles::ceph::client on the k8s (osd) and
# incus (mon/mgr/mds) roles. fsid/mons are overridden per-role in the role hiera.
# public_network is the /32 of every ceph host's ceph-public loopback.
profiles::ceph::client::cluster_public_ips:
- 198.18.23.1
- 198.18.23.2
- 198.18.23.3
- 198.18.23.4
- 198.18.23.5
- 198.18.23.6
- 198.18.23.7
- 198.18.23.8
- 198.18.23.9
- 198.18.23.10
- 198.18.23.11
- 198.18.23.12
- 198.18.23.13
- 198.18.23.14
- 198.18.23.15
- 198.18.23.16
- 198.18.23.17
- 198.18.23.18
- 198.18.23.19
profiles::ceph::client::mon_initial_members:
- prodnxsr0009
- prodnxsr0010
- prodnxsr0011
- prodnxsr0012
- prodnxsr0013
# two mds daemon instances per mon/mgr/mds host (rendered only where
# render_mds_config is true, i.e. the incus node role).
profiles::ceph::client::mds_instances:
prodnxsr0009: 2
prodnxsr0010: 2
prodnxsr0011: 2
prodnxsr0012: 2
prodnxsr0013: 2
profiles::ceph::conf::config:
global:
auth_client_required: 'cephx'
@@ -2,6 +2,6 @@
profiles::consul::server::bootstrap_count: 3
profiles::consul::server::raft_multiplier: 10
profiles::consul::server::primary_datacenter: 'au-syd1'
profiles::consul::server::join_remote_regions: false
profiles::consul::server::join_remote_regions: true
profiles::consul::server::remote_regions:
- syd1
-8
View File
@@ -7,11 +7,3 @@ profiles_dns_upstream_forwarder_consul:
- 198.18.19.14
profiles_dns_upstream_forwarder_k8s:
- 198.18.19.20
profiles::consul::client::members_lookup: false
# static: k8s-compiled hosts can't see the servers in PuppetDB; update when servers change
profiles::consul::client::consul_servers:
- ausyd1nxvm2005.main.unkin.net
- ausyd1nxvm2006.main.unkin.net
- ausyd1nxvm2007.main.unkin.net
- ausyd1nxvm2008.main.unkin.net
- ausyd1nxvm2009.main.unkin.net
@@ -3,7 +3,23 @@ haproxy_server_k8s_syd1_traefik_internal: 'k8s-traefik-internal 198.18.200.4:443
haproxy_server_k8s_syd1_traefik_external: 'k8s-traefik-external 198.18.199.0:443 ssl verify none check inter 2s rise 3 fall 2'
profiles::haproxy::dns::ipaddr: "%{hiera('anycast_ip')}"
profiles::haproxy::dns::vrrp_cnames: []
profiles::haproxy::dns::vrrp_cnames:
- sonarr.main.unkin.net
- radarr.main.unkin.net
- lidarr.main.unkin.net
- readarr.main.unkin.net
- prowlarr.main.unkin.net
- nzbget.main.unkin.net
- git.unkin.net
- fafflix.unkin.net
- grafana.unkin.net
- dashboard.ceph.unkin.net
- mail-webadmin.main.unkin.net
- mail-in.main.unkin.net
- mail.main.unkin.net
- autoconfig.main.unkin.net
- autodiscover.main.unkin.net
- auth.unkin.net
profiles::haproxy::mappings:
fe_http:
@@ -416,7 +432,9 @@ profiles::pki::vault::alt_names:
- mail-webadmin.main.unkin.net
# additional cnames
profiles::haproxy::dns::cnames: []
profiles::haproxy::dns::cnames:
- au-syd1-pve.main.unkin.net
- au-syd1-pve-api.main.unkin.net
# letsencrypt certificates
certbot::client::service: haproxy
@@ -2,6 +2,6 @@
profiles::consul::server::bootstrap_count: 3
profiles::consul::server::raft_multiplier: 10
profiles::consul::server::primary_datacenter: 'au-syd1'
profiles::consul::server::join_remote_regions: false
profiles::consul::server::join_remote_regions: true
profiles::consul::server::remote_regions:
- drw1
@@ -1,45 +0,0 @@
---
# primary interface is the WAN uplink; pin host identity to the dum0 loopback
networking_loopback0_ip: 198.18.2.160
networking_loopback1_ip: 198.18.21.160
# dns: keep the local dnsmasq resolver
profiles::dns::base::nameservers:
- 127.0.0.1
profiles::dns::base::search:
- main.unkin.net
profiles::dns::base::primary_interface: dum0
profiles::dns::updater::deny_ranges:
- 198.18.199.0/24
- 198.18.200.0/24
- 10.42.0.0/16
- 10.43.0.0/16
- 10.10.12.0/24 # wg0
- 103.216.190.0/23 # wan uplink
profiles::consul::client::host_addr: "%{hiera('networking_loopback0_ip')}"
# ssh: listen on localhost and dum0 only; knock out the common wan primary ip
lookup_options:
ssh::server::options:
merge:
strategy: deep
knockout_prefix: '--'
ssh::server::options:
ListenAddress:
- "--%{facts.networking.ip}"
- 127.0.0.1
- "%{hiera('networking_loopback0_ip')}"
profiles::ssh::sign::principals:
- "%{hiera('networking_loopback0_ip')}"
# frrouting
frrouting::ospfd_router_id: "%{hiera('networking_loopback0_ip')}"
frrouting::ospfd_interfaces:
dum0:
area: 0.0.0.0
dum1:
area: 0.0.0.0
bond0.201:
area: 0.0.0.0
frrouting::ospf_preferred_source_enable: true
frrouting::ospf_preferred_source: "%{hiera('networking_loopback1_ip')}"
-4
View File
@@ -77,7 +77,6 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
rpm-vendor:
name: rpm-vendor
descr: rpm-vendor repository
@@ -85,7 +84,6 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
# Per-release variants, resolved from the host's EL major version so el8
# hosts pull rpm-internal-el8/rpm-vendor-el8, el9 hosts el9, etc.
rpm-internal-release:
@@ -95,7 +93,6 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal-el%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
rpm-vendor-release:
name: rpm-vendor-el%{facts.os.release.major}
descr: rpm-vendor-el%{facts.os.release.major} repository
@@ -103,7 +100,6 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-el%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
# Additional repositories - default to absent, roles can override with ensure: present
# FRRouting repositories
-2
View File
@@ -60,7 +60,6 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-internal-f%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
rpm-vendor:
name: rpm-vendor-f%{facts.os.release.major}
descr: rpm-vendor-f%{facts.os.release.major} repository
@@ -68,4 +67,3 @@ profiles::yum::global::repos:
baseurl: https://artifactapi.k8s.syd1.au.unkin.net/api/v1/local/rpm-vendor-f%{facts.os.release.major}/
gpgcheck: false
mirrorlist: absent
metadata_expire: '60'
-5
View File
@@ -2,8 +2,6 @@
hiera_include:
- profiles::nginx::simpleproxy
profiles::puppet::migrate::enabled: true
profiles::yum::global::repos:
ceph:
ensure: present
@@ -67,9 +65,6 @@ profiles::nginx::simpleproxy::locations:
- 127.0.0.1
- "%{facts.networking.ip}"
- 198.18.24.0/24
- 198.18.21.0/24
- 198.18.15.0/24
- 198.18.19.0/24
location_deny:
- all
# authorised access from external
+1 -5
View File
@@ -14,8 +14,6 @@ hiera_include:
- profiles::storage::cephfsvols
- exporters::frr_exporter
profiles::puppet::migrate::enabled: true
# FIXME: puppet-python wants to try manage python-dev, which is required by the ceph package
python::manage_dev_package: false
@@ -205,9 +203,7 @@ profiles::accounts::sysadmin::extra_groups:
- incus-admin
# manage cephfs mounts
profiles::ceph::client::manage_ceph_conf: true
# mon/mgr/mds host: render the [mds] + [mds.*] sections
profiles::ceph::client::render_mds_config: true
profiles::ceph::client::manage_ceph_conf: false
profiles::ceph::client::manage_ceph_package: false
profiles::ceph::client::manage_ceph_paths: false
profiles::ceph::client::fsid: 'de96a98f-3d23-465a-a899-86d3d67edab8'
+1 -5
View File
@@ -8,13 +8,9 @@ hiera_include:
- frrouting
- rke2
profiles::puppet::migrate::enabled: true
# 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:
@@ -94,7 +90,7 @@ profiles::packages::include:
profiles::selinux::setenforce::mode: disabled
profiles::ceph::client::manage_ceph_conf: true
profiles::ceph::client::manage_ceph_conf: false
profiles::ceph::client::manage_ceph_package: false
profiles::ceph::client::manage_ceph_paths: false
profiles::ceph::client::fsid: 'de96a98f-3d23-465a-a899-86d3d67edab8'
-3
View File
@@ -30,9 +30,6 @@ rke2::config_hash:
- '--node-monitor-period=4s'
protect-kernel-defaults: true
disable-kube-proxy: false
# taint the control-plane/etcd nodes so only tolerating workloads schedule here
node-taint:
- "node-role.kubernetes.io/control-plane=true:NoSchedule"
# configure consul service
consul::services:
+1 -1
View File
@@ -33,7 +33,7 @@ profiles::packages::include:
profiles::selinux::setenforce::mode: disabled
profiles::ceph::client::manage_ceph_conf: true
profiles::ceph::client::manage_ceph_conf: false
profiles::ceph::client::manage_ceph_package: false
profiles::ceph::client::manage_ceph_paths: false
profiles::ceph::client::fsid: 'de96a98f-3d23-465a-a899-86d3d67edab8'
-30
View File
@@ -1,30 +0,0 @@
---
hiera_include:
- frrouting
- exporters::frr_exporter
# routing
sysctl::base::values:
net.ipv4.ip_forward:
value: '1'
net.ipv4.conf.all.rp_filter:
value: '0'
net.ipv4.conf.default.rp_filter:
value: '0'
# overrides 50-redhat.conf's per-interface rp_filter=1 (applied by udev on link add); sysctl -n can't glob, so no enforce
net.ipv4.conf.*.rp_filter:
value: '0'
enforce: false
# frrouting
exporters::frr_exporter::enable: true
frrouting::ospfd_redistribute:
- connected
frrouting::daemons:
ospfd: true
# consul
profiles::consul::client::node_rules:
- resource: service
segment: frr_exporter
disposition: write
+3 -16
View File
@@ -1,6 +1,5 @@
---
profiles::puppet::autosign::subnet_ranges:
- '198.18.2.0/24'
- '198.18.13.0/24'
- '198.18.14.0/24'
- '198.18.15.0/24'
@@ -27,19 +26,6 @@ profiles::puppet::cobbler_enc::packages:
- 'requests'
- 'PyYAML'
profiles::puppet::enc::repo: https://git.service.au-syd1.consul/unkinben/puppet-enc.git
# Deep-merged with the entries in roles/infra/puppet.yaml.
profiles::packages::include:
encapic:
ensure: '0.2.0'
certmanager:
ensure: '0.2.0'
sshsignhost:
ensure: '0.1.0'
profiles::puppet::encapic::encapi_url: https://encapi.k8s.syd1.au.unkin.net
profiles::puppet::server::external_nodes: '/usr/bin/encapic-enc'
profiles::puppet::r10k::r10k_repo: https://git.unkin.net/unkin/puppet-r10k.git
profiles::puppet::g10k::bin_path: '/usr/bin/g10k'
profiles::puppet::g10k::cfg_path: '/etc/puppetlabs/r10k/r10k.yaml'
@@ -61,9 +47,10 @@ profiles::helpers::certmanager::vault_config:
profiles::helpers::sshsignhost::vault_config:
addr: 'https://vault.service.consul:8200'
mount_point: 'sshca'
mount_point: 'ssh-host-signer'
approle_path: 'approle'
role_name: 'signhost'
role_name: 'hostrole'
output_path: '/tmp/sshsignhost'
role_id: "%{lookup('sshsignhost::role_id')}"
profiles::puppet::server::agent_server: 'puppet.query.consul'
-24
View File
@@ -13,19 +13,9 @@ profiles::consul::server::addresses:
grpc_tls: "%{::networking.ip}"
profiles::consul::server::ports:
grpc: 8502
grpc_tls: 8503
dns: 8600
http: 8500
https: -1
profiles::consul::server::tls:
defaults:
ca_file: /etc/pki/ca-trust/source/anchors/vaultcaroot.pem
cert_file: /etc/pki/tls/vault/full_chain.crt
key_file: /etc/pki/tls/vault/private.key
internal_rpc:
verify_incoming: false
verify_outgoing: false
verify_server_hostname: false
profiles::consul::server::acl:
enabled: true
default_policy: 'deny'
@@ -41,8 +31,6 @@ profiles::pki::vault::alt_names:
- consul.service.consul
- "consul.service.%{facts.country}-%{facts.region}.consul"
- consul
- "server.%{facts.country}-%{facts.region}.consul"
- "%{facts.networking.fqdn}.server.%{facts.country}-%{facts.region}.consul"
# manage a simple nginx reverse proxy
profiles::nginx::simpleproxy::nginx_vhost: 'consul.service.consul'
@@ -58,18 +46,6 @@ profiles::consul::client::node_rules:
- resource: service
segment: frr_exporter
disposition: write
- resource: service
segment: mesh-gateway
disposition: write
- resource: service_prefix
segment: ''
disposition: read
- resource: node_prefix
segment: ''
disposition: read
- resource: agent_prefix
segment: ''
disposition: read
profiles::consul::prepared_query::rules:
vault:
ensure: 'present'
-8
View File
@@ -40,11 +40,3 @@ profiles::packages::include:
ensure: '0.1.1'
openbao-plugin-secrets-gitea:
ensure: '0.1.0'
openbao-plugin-secrets-apptoken:
ensure: '0.1.0'
openbao-plugin-secrets-netbox:
ensure: '0.1.0'
openbao-plugin-secrets-ghp:
ensure: '0.1.0'
openbao-plugin-secrets-arrstack:
ensure: '0.2.0'
-5
View File
@@ -1,8 +1,3 @@
---
# 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')}": {}
-51
View File
@@ -1,51 +0,0 @@
# manage dnsmasq as a dns forwarder and dhcp relay
class dnsmasq (
Boolean $manage_package = true,
Boolean $manage_service = true,
String $package_name = 'dnsmasq',
String $service_name = 'dnsmasq',
Stdlib::Absolutepath $config_file = '/etc/dnsmasq.conf',
Stdlib::Absolutepath $config_dir = '/etc/dnsmasq.d',
Boolean $purge_config_dir = false,
Array[String] $interfaces = [],
Array[Stdlib::IP::Address] $listen_addresses = ['127.0.0.1'],
Enum['bind-interfaces', 'bind-dynamic', 'none'] $bind_mode = 'bind-interfaces',
Boolean $no_resolv = false,
Array[String] $servers = [],
Hash[String, Array[String]] $forwards = {},
Optional[Integer[0]] $cache_size = undef,
Boolean $domain_needed = true,
Boolean $bogus_priv = true,
Array[String] $dhcp_relays = [],
Array[String] $options = [],
) {
if $manage_package {
package { $package_name:
ensure => installed,
before => File[$config_file, $config_dir],
}
}
file { $config_dir:
ensure => directory,
recurse => $purge_config_dir,
purge => $purge_config_dir,
}
file { $config_file:
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('dnsmasq/dnsmasq.conf.erb'),
}
if $manage_service {
service { $service_name:
ensure => running,
enable => true,
subscribe => File[$config_file, $config_dir],
}
}
}
@@ -1,40 +0,0 @@
# THIS FILE IS MANAGED BY PUPPET
user=dnsmasq
group=dnsmasq
conf-dir=<%= @config_dir %>,.rpmnew,.rpmsave,.rpmorig
<% @interfaces.each do |iface| -%>
interface=<%= iface %>
<% end -%>
<% unless @listen_addresses.empty? -%>
listen-address=<%= @listen_addresses.join(',') %>
<% end -%>
<% unless @bind_mode == 'none' -%>
<%= @bind_mode %>
<% end -%>
<% if @no_resolv -%>
no-resolv
<% end -%>
<% if @domain_needed -%>
domain-needed
<% end -%>
<% if @bogus_priv -%>
bogus-priv
<% end -%>
<% if @cache_size -%>
cache-size=<%= @cache_size %>
<% end -%>
<% @servers.each do |server| -%>
server=<%= server %>
<% end -%>
<% @forwards.keys.sort.each do |domain| -%>
<% @forwards[domain].each do |server| -%>
server=/<%= domain %>/<%= server %>
<% end -%>
<% end -%>
<% @dhcp_relays.each do |relay| -%>
dhcp-relay=<%= relay %>
<% end -%>
<% @options.each do |line| -%>
<%= line %>
<% end -%>
+12 -33
View File
@@ -3,21 +3,13 @@
require 'facter'
require 'yaml'
require 'net/http'
require 'openssl'
require 'uri'
require 'fileutils'
# EncapiENC module: Fetches ENC data from encapi, caches it, and provides structured facts.
module EncapiENC
# CobblerENC module: Fetches ENC data from Cobbler, caches it, and provides structured facts.
module CobblerENC
CACHE_FILE = '/var/cache/puppet_enc.yaml'
CACHE_TTL = 7 * 24 * 60 * 60 # 7 days in seconds
# Facter runs under Puppet's vendored ruby, whose OpenSSL trusts only
# /opt/puppetlabs/puppet/ssl/cert.pem and never the system trust store, so the
# estate CA anchor profiles::pki::vaultca installs has to be named explicitly.
CA_BUNDLE_PATHS = [
'/etc/pki/ca-trust/source/anchors/vaultcaroot.pem',
'/usr/local/share/ca-certificates/vaultcaroot.pem'
].freeze
@enc_data = nil # In-memory cache for the ENC response
def self.read_cache
@@ -37,22 +29,9 @@ module EncapiENC
File.write(CACHE_FILE, cache_data.to_yaml)
end
def self.ca_bundle
CA_BUNDLE_PATHS.find { |path| File.exist?(path) }
end
def self.http_client(uri)
client = Net::HTTP.new(uri.host, uri.port)
client.use_ssl = true
client.verify_mode = OpenSSL::SSL::VERIFY_PEER
bundle = ca_bundle
client.ca_file = bundle if bundle
client
end
def self.fetch_from_encapi
uri = URI("https://encapi.k8s.syd1.au.unkin.net/cblr/svc/op/puppet/hostname/#{Facter.value(:fqdn) || Facter.value(:hostname)}")
response = http_client(uri).request(Net::HTTP::Get.new(uri))
def self.fetch_from_cobbler
uri = URI("http://cobbler.main.unkin.net/cblr/svc/op/puppet/hostname/#{Facter.value(:fqdn) || Facter.value(:hostname)}")
response = Net::HTTP.get_response(uri)
raise "Failed to fetch ENC data. HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
@@ -62,7 +41,7 @@ module EncapiENC
def self.retrieve_enc_data
return @enc_data if @enc_data
@enc_data = fetch_from_encapi
@enc_data = fetch_from_cobbler
write_cache(@enc_data)
@enc_data
end
@@ -70,26 +49,26 @@ module EncapiENC
def self.fetch_enc_data
retrieve_enc_data
rescue StandardError => e
Facter.warn("Error retrieving encapi ENC data: #{e.message}")
Facter.warn("Error retrieving Cobbler ENC data: #{e.message}")
@enc_data = read_cache
return @enc_data unless @enc_data.empty?
raise 'No cached ENC data available and encapi is unreachable.'
raise 'No cached ENC data available and Cobbler is down.'
end
def self.enc_role
fetch_enc_data.fetch('classes', {}).keys.first || raise('ENC Role not found in encapi ENC response')
fetch_enc_data.fetch('classes', {}).keys.first || raise('ENC Role not found in Cobbler ENC response')
end
def self.enc_env
fetch_enc_data.fetch('environment', nil) || raise('ENC Environment not found in encapi ENC response')
fetch_enc_data.fetch('environment', nil) || raise('ENC Environment not found in Cobbler ENC response')
end
end
Facter.add('enc_role') do
setcode { EncapiENC.enc_role }
setcode { CobblerENC.enc_role }
end
Facter.add('enc_env') do
setcode { EncapiENC.enc_env }
setcode { CobblerENC.enc_env }
end
-104
View File
@@ -1,104 +0,0 @@
# 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
-1
View File
@@ -5,7 +5,6 @@ require 'ipaddr'
# a class that creates facts based on the subnet
class SubnetAttributes
SUBNET_TO_ATTRIBUTES = {
'198.18.2.0/24' => { environment: 'prod', region: 'syd1', country: 'au', zone: 'common' }, # router loopbacks
'198.18.13.0/24' => { environment: 'prod', region: 'syd1', country: 'au', zone: 'common' },
'198.18.14.0/24' => { environment: 'prod', region: 'syd1', country: 'au', zone: 'common' },
'198.18.15.0/24' => { environment: 'prod', region: 'syd1', country: 'au', zone: 'common' },
+2 -2
View File
@@ -6,8 +6,8 @@ class nzbget (
$manage_group = $nzbget::params::manage_group,
$service_enable = $nzbget::params::service_enable,
$service_name = $nzbget::params::service_name,
$bind_address = $nzbget::params::bind_address,
$port = $nzbget::params::port,
$bind_address = $sonarr::params::bind_address,
$port = $sonarr::params::port,
) inherits nzbget::params {
include nzbget::install
+1 -8
View File
@@ -6,17 +6,10 @@ class rke2::install (
Stdlib::HTTPUrl $container_archive_source = $rke2::container_archive_source,
){
# versionlock rke2 before install so the lock exists before any upgrade is attempted
# versionlock rke2
yum::versionlock{"rke2-${node_type}":
ensure => present,
version => "${rke2_version}~${rke2_release}",
before => Package["rke2-${node_type}"],
}
# lock rke2-common (a strict = version dep) so the rolling latest channel can't drift it ahead of the pinned server/agent
yum::versionlock{'rke2-common':
ensure => present,
version => "${rke2_version}~${rke2_release}",
}
# install rke2
+3 -3
View File
@@ -1,8 +1,8 @@
# rke2 params
class rke2::params (
Enum['server', 'agent'] $node_type = 'agent',
String $rke2_version = '1.33.13',
String $rke2_release = 'rke2r2',
String $rke2_version = '1.33.11',
String $rke2_release = 'rke2r1',
Stdlib::Absolutepath $config_file = '/etc/rancher/rke2/config.yaml',
Hash $config_hash = {},
Stdlib::HTTPSUrl $join_url = 'https://127.0.0.1:9345',
@@ -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 = {},
) {}
-44
View File
@@ -1,44 +0,0 @@
# manage wireguard interfaces via wg-quick
class wireguard (
Boolean $manage_package = true,
String $package_name = 'wireguard-tools',
Variant[Hash, Sensitive[Hash]] $interfaces = {},
) {
if $manage_package {
package { $package_name:
ensure => installed,
before => File['/etc/wireguard'],
}
}
file { '/etc/wireguard':
ensure => directory,
owner => 'root',
group => 'root',
mode => '0700',
}
# hiera hands eyaml secrets over as plain strings inside the (Sensitive) hash; re-wrap them per resource
$raw = $interfaces ? {
Sensitive => $interfaces.unwrap,
default => $interfaces,
}
$raw.each |String $iface, Hash $data| {
$peers = $data.get('peers', []).map |Hash $peer| {
$peer['preshared_key'] =~ String ? {
true => $peer + { 'preshared_key' => Sensitive($peer['preshared_key']) },
default => $peer,
}
}
$private_key = $data['private_key'] =~ String ? {
true => Sensitive($data['private_key']),
default => $data['private_key'],
}
wireguard::interface { $iface:
* => $data + { 'peers' => $peers, 'private_key' => $private_key },
}
}
}
-63
View File
@@ -1,63 +0,0 @@
# manage one wg-quick interface; without private_key, /etc/wireguard/<iface>.key is generated once and loaded via PostUp
define wireguard::interface (
Array[Stdlib::IP::Address] $addresses,
Optional[Stdlib::Port] $listen_port = undef,
Optional[Integer[1280, 9000]] $mtu = undef,
Optional[Sensitive[String[1]]] $private_key = undef,
Array[Struct[{
public_key => String[1],
allowed_ips => Variant[String[1], Array[String[1], 1]],
preshared_key => Optional[Sensitive[String[1]]],
endpoint => Optional[String[1]],
persistent_keepalive => Optional[Integer[0, 65535]],
}]] $peers = [],
) {
$conf = "/etc/wireguard/${name}.conf"
$key = $private_key.then |$k| { $k.unwrap }
if $private_key =~ Undef {
$keyfile = "/etc/wireguard/${name}.key"
exec { "wireguard_genkey_${name}":
command => "/bin/sh -c 'umask 077; wg genkey > ${keyfile}'",
creates => $keyfile,
path => ['/usr/bin', '/usr/sbin', '/bin', '/sbin'],
require => File['/etc/wireguard'],
}
file { $keyfile:
ensure => file,
owner => 'root',
group => 'root',
mode => '0600',
require => Exec["wireguard_genkey_${name}"],
before => [File[$conf], Service["wg-quick@${name}"]],
}
}
file { $conf:
ensure => file,
owner => 'root',
group => 'root',
mode => '0600',
content => Sensitive(template('wireguard/wg.conf.erb')),
show_diff => false,
notify => Exec["wireguard_syncconf_${name}"],
}
service { "wg-quick@${name}":
ensure => running,
enable => true,
require => File[$conf],
}
# syncconf applies peer/key changes without bouncing the tunnel; address/mtu changes need a manual restart
exec { "wireguard_syncconf_${name}":
command => "/bin/bash -c 'wg syncconf ${name} <(wg-quick strip ${name})'",
onlyif => "/usr/sbin/ip link show ${name}",
path => ['/usr/bin', '/usr/sbin', '/bin', '/sbin'],
refreshonly => true,
require => Service["wg-quick@${name}"],
}
}
-31
View File
@@ -1,31 +0,0 @@
# THIS FILE IS MANAGED BY PUPPET
[Interface]
<% @addresses.each do |addr| -%>
Address = <%= addr %>
<% end -%>
<% if @listen_port -%>
ListenPort = <%= @listen_port %>
<% end -%>
<% if @mtu -%>
MTU = <%= @mtu %>
<% end -%>
<% if @key -%>
PrivateKey = <%= @key %>
<% else -%>
PostUp = wg set %i private-key /etc/wireguard/%i.key
<% end -%>
<% @peers.each do |peer| -%>
[Peer]
PublicKey = <%= peer['public_key'] %>
<% if peer['preshared_key'] -%>
PresharedKey = <%= peer['preshared_key'].unwrap %>
<% end -%>
AllowedIPs = <%= Array(peer['allowed_ips']).join(', ') %>
<% if peer['endpoint'] -%>
Endpoint = <%= peer['endpoint'] %>
<% end -%>
<% if peer['persistent_keepalive'] -%>
PersistentKeepalive = <%= peer['persistent_keepalive'] %>
<% end -%>
<% end -%>
+3 -31
View File
@@ -2,9 +2,6 @@
class profiles::ceph::client (
String $fsid,
Array[Stdlib::Host] $mons,
# cluster topology (single source of truth: hieradata/common.yaml)
Array[Stdlib::Host] $cluster_public_ips,
Array[Stdlib::Host] $mon_initial_members,
Stdlib::Absolutepath $config_file = '/etc/ceph/ceph.conf',
Boolean $manage_ceph_conf = true,
Boolean $manage_ceph_package = true,
@@ -13,24 +10,6 @@ class profiles::ceph::client (
String $group = 'ceph',
Stdlib::Filemode $mode = '0644',
Hash $keyrings = {},
# [global] tunables (defaults match the live hand-maintained ceph.conf)
String $auth_client_required = 'cephx',
String $auth_cluster_required = 'cephx',
String $auth_service_required = 'cephx',
Boolean $mon_allow_pool_delete = true,
Boolean $ms_bind_ipv4 = true,
Boolean $ms_bind_ipv6 = false,
Integer $osd_crush_chooseleaf_type = 1,
Integer $osd_pool_default_min_size = 2,
Integer $osd_pool_default_size = 3,
Integer $osd_pool_default_pg_num = 128,
# mds config sections; only rendered on mon/mgr/mds hosts (render_mds_config)
Boolean $render_mds_config = false,
Hash[String, Integer] $mds_instances = {},
Hash $mds_common = {
'keyring' => '/var/lib/ceph/mds/ceph-$id/keyring',
'mds_standby_replay' => true,
},
) {
# dont run this on proxmox nodes
@@ -43,13 +22,6 @@ class profiles::ceph::client (
}
}
# only depend on the package when this class manages it; on the ceph
# hosts the package is delivered by cephadm / profiles::packages instead.
$config_require = $manage_ceph_package ? {
true => Package['ceph-common'],
default => undef,
}
# manage the ceph directory
if $manage_ceph_paths {
file { '/etc/ceph':
@@ -57,11 +29,11 @@ class profiles::ceph::client (
owner => $owner,
group => $group,
mode => $mode,
require => $config_require,
require => Package['ceph-common'],
}
}
# render /etc/ceph/ceph.conf from cluster topology in hiera
# create a basic client config
if $manage_ceph_conf {
file { $config_file:
ensure => file,
@@ -69,7 +41,7 @@ class profiles::ceph::client (
group => $group,
mode => $mode,
content => template('profiles/ceph/client.conf.erb'),
require => $config_require,
require => Package['ceph-common'],
}
}
-3
View File
@@ -2,9 +2,6 @@ class profiles::ceph::osd (
Boolean $ensure_running = true,
) {
# tune the I/O scheduler on the disks backing ceph OSDs
include profiles::ceph::osd_scheduler
if $ensure_running and $facts['is_ceph_osd'] {
$facts['ceph_services']['osd'].each |String $svc| {
service { $svc:
@@ -1,32 +0,0 @@
class profiles::ceph::osd_scheduler (
String[1] $scheduler = 'none',
) {
$devices = $facts['ceph_osd_devices']
# no-op where the fact is absent/empty (VMs, non-OSD hosts have no ceph PVs)
if $devices =~ Array[String[1], 1] {
# strip /dev/ so the rule matches the udev KERNEL sysname (e.g. sda)
$kernel_names = $devices.map |$dev| { regsubst($dev, '^.*/', '') }
$sysname_matches = $kernel_names.map |$name| { "--sysname-match=${name}" }
file { '/etc/udev/rules.d/60-ceph-osd-scheduler.rules':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('profiles/ceph/osd-scheduler.rules.erb'),
notify => Exec['ceph-osd-scheduler-reload'],
}
# apply immediately; udev re-applies on reboot and device re-add
$trigger = "udevadm trigger --subsystem-match=block --action=change ${join($sysname_matches, ' ')}"
exec { 'ceph-osd-scheduler-reload':
command => "udevadm control --reload-rules && ${trigger}",
path => ['/usr/bin', '/bin', '/usr/sbin', '/sbin'],
refreshonly => true,
}
}
}
@@ -1,52 +0,0 @@
# profiles::consul::mesh_gateway
class profiles::consul::mesh_gateway (
Stdlib::IP::Address $address = $facts['networking']['ip'],
Stdlib::Port $port = 8443,
Stdlib::Port $http_port = 8500,
Stdlib::Port $grpc_port = 8502,
Stdlib::Absolutepath $token_file = '/etc/consul.d/mesh-gateway.token',
) {
include profiles::consul::client
# include after profiles::consul::server, which declares Class['consul'] only once /data is mounted
if defined(Class['consul']) {
package { 'envoy':
ensure => installed,
}
file { $token_file:
ensure => file,
owner => 'consul',
group => 'consul',
mode => '0600',
require => Class['consul'],
content => Sensitive(fqdn_uuid("${facts['networking']['fqdn']}-${profiles::consul::client::secret_id_salt}")),
}
systemd::unit_file { 'consul-mesh-gateway.service':
enable => true,
active => true,
content => @("UNIT"),
[Unit]
Description=Consul mesh gateway (envoy)
Requires=consul.service
After=consul.service
[Service]
User=consul
Group=consul
ExecStart=/usr/bin/consul connect envoy -gateway=mesh -register -service=mesh-gateway -expose-servers \
-address=${address}:${port} -wan-address=${address}:${port} \
-http-addr=http://${address}:${http_port} -grpc-addr=http://${address}:${grpc_port} \
-token-file=${token_file}
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
| UNIT
require => [Package['envoy'], Class['consul']],
subscribe => File[$token_file],
}
}
}
+1 -4
View File
@@ -11,7 +11,6 @@ class profiles::consul::server (
Hash $acl = {},
Hash $ports = {},
Hash $addresses = {},
Hash $tls = {},
Boolean $members_lookup = false,
String $members_role = undef,
Array $consul_servers = [],
@@ -113,8 +112,6 @@ class profiles::consul::server (
'acl' => $acl,
'ports' => $ports,
'addresses' => $addresses,
'tls' => $tls,
'auto_reload_config' => true,
'disable_remote_exec' => $disable_remote_exec,
'disable_update_check' => $disable_update_check,
'domain' => $domain,
@@ -132,7 +129,7 @@ class profiles::consul::server (
'advertise_addr' => $advertise_addr,
'retry_join' => $servers_array,
'retry_join_wan' => $remote_servers_array,
'connect' => { 'enabled' => true, 'enable_mesh_gateway_wan_federation' => true },
'connect' => { 'enabled' => true },
'recursors' => ['198.18.19.16'],
},
}
+2 -1
View File
@@ -24,7 +24,8 @@ class profiles::dns::updater (
Stdlib::AbsolutePath $config_dir = '/etc/dns-updater',
Stdlib::AbsolutePath $master_basedir = lookup('profiles::dns::master::basedir'),
# dns-updater daemon (replaces the dns-update shell script). 'latest' so hosts
# pick up new releases (e.g. the record filter).
# pick up new releases (e.g. the record filter); rpm-internal metadata_expire
# is 1h so this does not thrash.
String $package_ensure = 'latest',
Stdlib::AbsolutePath $api_socket = '/run/dns-updater/api.sock',
String $resync = '10m',
+63 -14
View File
@@ -1,28 +1,77 @@
# profiles::helpers::certmanager
#
# renders the config.yaml read by the certmanager binary (RPM-installed)
# wrapper class for python, pip and venv
class profiles::helpers::certmanager (
String $script_name = 'certmanager',
Stdlib::AbsolutePath $base_path = "/opt/${script_name}",
Stdlib::AbsolutePath $venv_path = "${base_path}/venv",
Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml",
Hash $vault_config = {},
String $owner = 'root',
String $group = 'root',
Boolean $systempkgs = false,
String $version = 'system',
Array[String[1]] $packages = ['requests', 'pyyaml'],
){
file { $base_path:
ensure => directory,
mode => '0755',
owner => $owner,
group => $group,
}
if $::facts['python3_version'] {
file { $config_path:
ensure => file,
mode => '0660',
owner => 'puppet',
group => 'root',
content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")),
require => File[$base_path],
$python_version = $version ? {
'system' => $::facts['python3_version'],
default => $version,
}
# ensure the base_path exists
file { $base_path:
ensure => directory,
mode => '0755',
owner => $owner,
group => $group,
}
# create a venv
python::pyvenv { $venv_path :
ensure => present,
version => $python_version,
systempkgs => $systempkgs,
venv_dir => $venv_path,
owner => $owner,
group => $group,
require => File[$base_path],
}
# install the required pip packages
$packages.each |String $package| {
python::pip { "${venv_path}_${package}":
ensure => present,
pkgname => $package,
virtualenv => $venv_path,
}
}
# create the script from a template
file { "${base_path}/${script_name}":
ensure => file,
mode => '0755',
content => template("profiles/helpers/${script_name}.erb"),
require => Python::Pyvenv[$venv_path],
}
# create the config from a template
file { $config_path:
ensure => file,
mode => '0660',
owner => 'puppet',
group => 'root',
content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")),
require => Python::Pyvenv[$venv_path],
}
# create symbolic link in $PATH
file { "/usr/local/bin/${script_name}":
ensure => 'link',
target => "${base_path}/${script_name}",
require => File["${base_path}/${script_name}"],
}
}
}
+63 -14
View File
@@ -1,28 +1,77 @@
# profiles::helpers::sshsignhost
#
# renders the config.yaml read by the sshsignhost binary (RPM-installed)
# wrapper class for python, pip and venv
class profiles::helpers::sshsignhost (
String $script_name = 'sshsignhost',
Stdlib::AbsolutePath $base_path = "/opt/${script_name}",
Stdlib::AbsolutePath $venv_path = "${base_path}/venv",
Stdlib::AbsolutePath $config_path = "${base_path}/config.yaml",
Hash $vault_config = {},
String $owner = 'root',
String $group = 'root',
Boolean $systempkgs = false,
String $version = 'system',
Array[String[1]] $packages = ['requests', 'pyyaml'],
){
file { $base_path:
ensure => directory,
mode => '0755',
owner => $owner,
group => $group,
}
if $::facts['python3_version'] {
file { $config_path:
ensure => file,
mode => '0660',
owner => 'puppet',
group => 'root',
content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")),
require => File[$base_path],
$python_version = $version ? {
'system' => $::facts['python3_version'],
default => $version,
}
# ensure the base_path exists
file { $base_path:
ensure => directory,
mode => '0755',
owner => $owner,
group => $group,
}
# create a venv
python::pyvenv { $venv_path :
ensure => present,
version => $python_version,
systempkgs => $systempkgs,
venv_dir => $venv_path,
owner => $owner,
group => $group,
require => File[$base_path],
}
# install the required pip packages
$packages.each |String $package| {
python::pip { "${venv_path}_${package}":
ensure => present,
pkgname => $package,
virtualenv => $venv_path,
}
}
# create the script from a template
file { "${base_path}/${script_name}":
ensure => file,
mode => '0755',
content => template("profiles/helpers/${script_name}.erb"),
require => Python::Pyvenv[$venv_path],
}
# create the config from a template
file { $config_path:
ensure => file,
mode => '0660',
owner => 'puppet',
group => 'root',
content => Sensitive(template("profiles/helpers/${script_name}_config.yaml.erb")),
require => Python::Pyvenv[$venv_path],
}
# create symbolic link in $PATH
file { "/usr/local/bin/${script_name}":
ensure => 'link',
target => "${base_path}/${script_name}",
require => File["${base_path}/${script_name}"],
}
}
}
-33
View File
@@ -1,33 +0,0 @@
# 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,
}
}
}
+1 -2
View File
@@ -60,11 +60,10 @@ class profiles::puppet::agent (
require => Yumrepo[$use_yumrepo],
}
# versionlock puppet-agent before install so the lock exists before any upgrade is attempted
# versionlock puppet-agent
yum::versionlock{$use_package:
ensure => $agent_versionlock_ensure,
version => $agent_versionlock_version,
before => Package[$use_package],
}
}
'Debian': {
-32
View File
@@ -1,32 +0,0 @@
# Class: profiles::puppet::encapic
#
# Manages the configuration for the encapic ENC client. The package itself is
# installed through profiles::packages (pinned in hiera); this class owns the
# config so the encapi endpoint can change without repackaging.
class profiles::puppet::encapic (
Stdlib::HTTPUrl $encapi_url,
Stdlib::AbsolutePath $config_dir = '/etc/encapic',
String $config_name = 'encapic.conf',
String $owner = 'root',
String $group = 'root',
) {
# The RPM ships this file as %config(noreplace), so puppet must write it only
# once the package is present or the install overwrites it.
file { $config_dir:
ensure => directory,
mode => '0755',
owner => $owner,
group => $group,
require => Package['encapic'],
}
file { "${config_dir}/${config_name}":
ensure => file,
mode => '0644',
owner => $owner,
group => $group,
content => "ENCAPI_URL=${encapi_url}\n",
require => File[$config_dir],
}
}
@@ -12,7 +12,6 @@ class profiles::puppet::puppetmaster (
include profiles::puppet::g10k
include profiles::puppet::enc
include profiles::puppet::cobbler_enc
include profiles::puppet::encapic
include profiles::puppet::autosign
include profiles::puppet::gems
include profiles::helpers::certmanager
-14
View File
@@ -130,20 +130,6 @@ class profiles::vault::server (
mode => '0600',
}
# go-plugin creates each secrets plugin's control socket under TMPDIR
# (/tmp/pluginNNN by default); systemd-tmpfiles-clean reaps aged /tmp files
# and severs the socket of a long-lived plugin, orphaning the process. Point
# TMPDIR at a per-start RuntimeDirectory on /run (tmpfs, no age cleanup).
systemd::manage_dropin { 'plugin-tmpdir.conf':
unit => 'vault.service',
service_entry => {
'RuntimeDirectory' => 'vault-plugins',
'RuntimeDirectoryMode' => '0700',
'Environment' => 'TMPDIR=/run/vault-plugins',
},
notify => Service['vault'],
}
service { 'vault':
ensure => true,
enable => true,
+2 -27
View File
@@ -1,28 +1,3 @@
[global]
auth_client_required = <%= @auth_client_required %>
auth_cluster_required = <%= @auth_cluster_required %>
auth_service_required = <%= @auth_service_required %>
fsid = <%= @fsid %>
mon_allow_pool_delete = <%= @mon_allow_pool_delete %>
mon_initial_members = <%= @mon_initial_members.join(',') %>
mon_host = <%= @mons.join(',') %>
ms_bind_ipv4 = <%= @ms_bind_ipv4 %>
ms_bind_ipv6 = <%= @ms_bind_ipv6 %>
osd_crush_chooseleaf_type = <%= @osd_crush_chooseleaf_type %>
osd_pool_default_min_size = <%= @osd_pool_default_min_size %>
osd_pool_default_size = <%= @osd_pool_default_size %>
osd_pool_default_pg_num = <%= @osd_pool_default_pg_num %>
public_network = <%= @cluster_public_ips.map { |ip| "#{ip}/32" }.join(',') %>
<% if @render_mds_config -%>
[mds]
keyring = <%= @mds_common['keyring'] %>
mds_standby_replay = <%= @mds_common['mds_standby_replay'] %>
<% @mds_instances.sort.each do |host, count| -%>
<% (1..count).each do |instance| -%>
[mds.<%= host %>-<%= instance %>]
host = <%= host %>
<% end -%>
<% end -%>
<% end -%>
fsid = <%= @fsid %>
mon_host = <%= @mons.join(' ') %>
@@ -1,5 +0,0 @@
# Managed by puppet (profiles::ceph::osd_scheduler).
# Set the I/O scheduler to <%= @scheduler %> on ceph OSD block devices.
<% @kernel_names.sort.each do |dev| -%>
ACTION=="add|change", SUBSYSTEM=="block", KERNEL=="<%= dev %>", ATTR{queue/scheduler}="<%= @scheduler %>"
<% end -%>
@@ -0,0 +1,102 @@
#!<%= @venv_path %>/bin/python
import argparse
import requests
import json
import os
import yaml
from zipfile import ZipFile
# remove this after certs are generated everywhere
requests.packages.urllib3.disable_warnings()
def load_config(config_path):
with open(config_path, 'r') as file:
config = yaml.safe_load(file)
return config['vault']
def authenticate_approle(vault_config):
url = f"{vault_config['addr']}/v1/auth/{vault_config['approle_path']}/login"
payload = {
"role_id": vault_config['role_id'],
}
response = requests.post(url, json=payload, verify=False)
if response.status_code == 200:
auth_response = response.json()
return auth_response['auth']['client_token']
else:
print(f"Error authenticating with AppRole: {response.text}")
return None
def request_certificate(common_name, alt_names, ip_sans, expiry_days, vault_config):
# Authenticate using AppRole and get a token
client_token = authenticate_approle(vault_config)
if not client_token:
print("Failed to authenticate with Vault using AppRole.")
return None
url = f"{vault_config['addr']}/v1/{vault_config['mount_point']}/issue/{vault_config['role_name']}"
headers = {'X-Vault-Token': client_token}
payload = {
"common_name": common_name,
"alt_names": ",".join(alt_names),
"ip_sans": ",".join(ip_sans),
"ttl": f"{expiry_days}d"
}
response = requests.post(url, headers=headers, json=payload, verify=False)
if response.status_code == 200:
return response.json()
else:
print(f"Error requesting certificate: {response.text}")
return None
def save_cert_files(certificate_response, common_name, compress, config, json_output):
base_path = config.get('output_path', '.')
cert_dir = os.path.join(base_path, common_name)
if json_output:
import json
output = {
'certificate': certificate_response['data']['certificate'],
'private_key': certificate_response['data']['private_key'],
'full_chain': certificate_response['data']['issuing_ca'] + "\n" + certificate_response['data']['certificate'],
}
print(json.dumps(output))
elif not compress:
os.makedirs(cert_dir, exist_ok=True)
with open(os.path.join(cert_dir, "certificate.crt"), "w") as cert_file:
cert_file.write(certificate_response['data']['certificate'])
with open(os.path.join(cert_dir, "private.key"), "w") as key_file:
key_file.write(certificate_response['data']['private_key'])
with open(os.path.join(cert_dir, "full_chain.crt"), "w") as full_chain_file:
full_chain_file.write(certificate_response['data']['issuing_ca'] + "\n" + certificate_response['data']['certificate'])
else:
zip_name = f"{os.path.join(base_path, common_name)}.zip"
with ZipFile(zip_name, 'w') as zipf:
zipf.writestr("certificate.crt", certificate_response['data']['certificate'])
zipf.writestr("private.key", certificate_response['data']['private_key'])
zipf.writestr("full_chain.crt", certificate_response['data']['issuing_ca'] + "\n" + certificate_response['data']['certificate'])
def main(config_file):
config = load_config(config_file)
parser = argparse.ArgumentParser(description='Request and retrieve a certificate from Vault.')
parser.add_argument('common_name', type=str, help='Common Name for the certificate')
parser.add_argument('-a', '--alt-names', type=str, default='', help='Comma-separated alternative names for the certificate')
parser.add_argument('-i', '--ip-sans', type=str, default='', help='Comma-separated IP Subject Alternative Names for the certificate')
parser.add_argument('-e', '--expiry-days', type=int, default=365, help='Validity of the certificate in days (default: 365)')
parser.add_argument('-c', '--compress', action='store_true', help='Compress the certificate, key, and full chain into a zip file')
parser.add_argument('--json', action='store_true', help='Output results in JSON format')
args = parser.parse_args()
alt_names = [name.strip() for name in args.alt_names.split(',') if name]
ip_sans = [ip.strip() for ip in args.ip_sans.split(',') if ip]
certificate_response = request_certificate(args.common_name, alt_names, ip_sans, args.expiry_days, config)
if certificate_response:
if args.json:
save_cert_files(certificate_response, args.common_name, args.compress, config, True)
else:
save_cert_files(certificate_response, args.common_name, args.compress, config, False)
else:
print("Failed to obtain certificate.")
if __name__ == "__main__":
config_file = '<%= @config_path %>'
main(config_file)
@@ -4,4 +4,4 @@ vault:
approle_path: '<%= @vault_config['approle_path'] %>'
mount_point: '<%= @vault_config['mount_point'] %>'
role_name: '<%= @vault_config['role_name'] %>'
output_path: '<%= @vault_config['output_path'] %>'
output_path: '<%= @vault_config['output_path'] %>'
@@ -0,0 +1,83 @@
#!<%= @venv_path %>/bin/python
import argparse
import requests
import json
import yaml
# remove this after certs are generated everywhere
requests.packages.urllib3.disable_warnings()
def load_config(config_path):
with open(config_path, 'r') as file:
config = yaml.safe_load(file)
return config['vault']
def authenticate_approle(vault_config):
url = f"{vault_config['addr']}/v1/auth/{vault_config['approle_path']}/login"
payload = {
"role_id": vault_config['role_id'],
}
response = requests.post(url, json=payload, verify=False)
if response.status_code == 200:
auth_response = response.json()
return auth_response['auth']['client_token']
else:
print(f"Error authenticating with AppRole: {response.text}")
return None
def sign_ssh_certificate(vault_config, public_key, valid_principals, ttl):
# Authenticate using AppRole and get a token
client_token = authenticate_approle(vault_config)
if not client_token:
print("Failed to authenticate with Vault using AppRole.")
return None
# Prepare the SSH certificate signing request
url = f"{vault_config['addr']}/v1/{vault_config['mount_point']}/sign/{vault_config['role_name']}"
headers = {'X-Vault-Token': client_token}
payload = {
"cert_type": "host",
"public_key": public_key,
"valid_principals": valid_principals,
"ttl": ttl
}
# Request the SSH certificate signing
response = requests.post(url, headers=headers, json=payload, verify=False)
if response.status_code == 200:
return response.json()
else:
print(f"Error requesting certificate: {response.text}")
return None
def main(config_file):
config = load_config(config_file)
parser = argparse.ArgumentParser(description='Sign SSH host certificate using Vault.')
parser.add_argument('--public_key', required=True, help='SSH public key as a string')
parser.add_argument('--valid_principals', required=True, help='Comma-separated list of valid principals')
parser.add_argument('--ttl', default='87600h', help='Time-to-live for the certificate (default: 87600h)')
parser.add_argument('--json', action='store_true', help='Output the resulting certificate as JSON')
args = parser.parse_args()
# Load configuration
config = load_config(config_file)
# Sign SSH certificate
response = sign_ssh_certificate(config, args.public_key, args.valid_principals, args.ttl)
if response and 'data' in response and 'signed_key' in response['data']:
if args.json:
output = {
'signed_key': response['data']['signed_key'],
}
print(json.dumps(output))
else:
print(response['data']['signed_key'])
else:
print("Error: The response does not contain the expected data.")
exit(1)
if __name__ == "__main__":
config_file = '<%= @config_path %>'
main(config_file)
@@ -4,3 +4,4 @@ vault:
approle_path: '<%= @vault_config['approle_path'] %>'
mount_point: '<%= @vault_config['mount_point'] %>'
role_name: '<%= @vault_config['role_name'] %>'
output_path: '<%= @vault_config['output_path'] %>'
@@ -1,12 +0,0 @@
# roles::infra::network::router
# an ospf router; frr only, interfaces and firewall are managed outside puppet
#
class roles::infra::network::router {
if $facts['firstrun'] {
include profiles::defaults
include profiles::firstrun::init
}else{
include profiles::defaults
include profiles::base
}
}
@@ -8,6 +8,5 @@ class roles::infra::storage::consul {
include profiles::base
include profiles::base::datavol
include profiles::consul::server
include profiles::consul::mesh_gateway
}
}