Commit Graph

133 Commits

Author SHA1 Message Date
unkin-agent b903c0d641 Keep wireguard keys Sensitive and generate missing private keys
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 successful
2026-10-03 20:57:00 +10:00
unkin-agent ec74484d89 Pin the journald ingest URL to port 443 (#533)
Estate journald ingestion has been down since 00:33Z. `systemd-journal-upload` appends `:19532/upload` to the configured URL whenever that URL carries no explicit port, so the portless k8s endpoint became `/insert/journald:19532/upload`, which vlinsert rejects as an unsupported path. The previous consul URL only worked because `:9428` was explicit.

- pin `victorialogs::client::journald::inserturl` to port 443

Requests then land on `/insert/journald/upload`, which vlinsert accepts.

Reviewed-on: #533
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 11:22:07 +10:00
unkin-agent c0e65fade3 Ship estate journald logs to the k8s log ingest endpoint (#531)
The estate ships journald to the VM VictoriaLogs cluster, which is being left to age out rather than grow. New log capacity lands in k8s, so clients need to point there while the VM cluster keeps serving historical queries until its retention lapses.

- repoint victorialogs::client::journald::inserturl at https://logs-ingest.k8s.syd1.au.unkin.net/insert/journald

VMs already trust the issuing CA via the system bundle, so journal-upload needs no TLS change.

Requires the k8s VLCluster deployed and serving /insert/journald first.

Reviewed-on: #531
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 10:01:49 +10:00
unkin-agent cb9f8870bf Trust the sshca host CA alongside the legacy signer (#530)
Catalog compilation moved to the k8s puppetserver compilers, which sign host certificates against the terraform-managed `sshca` mount. Clients only trust the legacy `ssh-host-signer` CA, so every re-signed node (ausyd1nxvm2120 already) presents a certificate nothing accepts, and knownhosts emits no plain host-key fallback.

- Add a second `@cert-authority *` entry for the `sshca` public key to `profiles::ssh::knownhosts::lines`.
- Keep the legacy entry untouched so legacy-signed hosts still verify.

Reviewed-on: #530
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-24 22:46:30 +10:00
unkinben 0fd01ac1a7 ceph: manage /etc/ceph/ceph.conf on osd and mon/mgr/mds hosts (#515)
## Why

`/etc/ceph/ceph.conf` on the prodnxsr ceph cluster (fsid `de96a98f`) is currently unmanaged and hand-maintained via the runbook (`profiles::ceph::client::manage_ceph_conf` is `false` on the k8s and incus roles). This brings it under Puppet, drives it from a single source of truth in hiera, and normalizes `public_network` to every ceph host's `/32`.

## How hosts are identified (osd-only vs mon/mgr/mds)

The authoritative, declarative signal is the role's `hiera_include` list (ENC `role` -> role hiera):

- **osd-only hosts** = `roles::infra::k8s::*` (prodnxsr0001-0008 and prodnxsr0014-0019): include `profiles::ceph::osd` + `::client`, NOT `::mds` -> `[global]` only.
- **mon/mgr/mds hosts** = `roles::infra::incus::node` (prodnxsr0009-0013): also include `profiles::ceph::mds` -> `[global]` + `[mds]` + `[mds.*]`.

The template keys on `profiles::ceph::client::render_mds_config`, set `true` only in the incus node role hiera (the single role that includes `profiles::ceph::mds`). The `is_ceph_mds` fact is a runtime fallback, not used here.

## Changes

- Add cluster topology as a single source of truth in `hieradata/common.yaml`: `cluster_public_ips` (all 19 ceph host `/32`s, 198.18.23.1-19), `mon_initial_members` (the 5 mons), and the `mds_instances` map (two mds daemons per mon/mgr/mds host).
- Rework `profiles/ceph/client.conf.erb` to render `[global]` always and the `[mds]`/`[mds.*]` sections only when `render_mds_config` is true, preserving the live two-space indent, exact key order, and blank-line separators between mds sections.
- Extend `profiles::ceph::client` with topology + tunable params (sample values as defaults) and gate `[mds]` on `render_mds_config`.
- Drop the hard `Package[ceph-common]` dependency when the class does not manage the package (cephadm / `profiles::packages` deliver it on the k8s and incus hosts; k8s hosts do not declare `ceph-common`, so an unconditional require would fail to compile).
- Enable `manage_ceph_conf: true` on `roles/infra/k8s.yaml`, `roles/infra/k8s/node.yaml`, and `roles/infra/incus/node.yaml`; add `render_mds_config: true` to the incus node role. prodnxsr0014-0019 run `roles::infra::k8s::compute` (confirmed live via PuppetDB; `is_ceph_osd` true, `is_ceph_mds` false), so they are already covered by `k8s.yaml` and get the `[global]`-only variant with no mds sections.

## Live-cluster safety review (please read before merge)

This touches a running storage cluster's config. The rendered content is byte-equivalent to the live file **except** for the one deliberate `public_network` normalization below.

- **Intended change:** `public_network` is normalized to all 19 ceph host `/32`s on every host (198.18.23.1-19). Adoption is now a **single `public_network`-line change on every cluster host**, no longer a no-op on the osd hosts:
  - osd hosts prodnxsr0001-0008 currently list `.1-.13` live -> first convergence adds `.14-.19`.
  - mon/mgr/mds hosts prodnxsr0009-0013 currently list only `.9-.13` live -> first convergence adds `.1-.8` and `.14-.19`.
  - prodnxsr0014-0019 (newly joined OSD hosts) get the file created with the full `[global]` variant.
- **Verified byte diff against the live files** (`/etc/ceph/ceph.conf` pulled from prodnxsr0008 and prodnxsr0009): each differs by **exactly one line** (the expanded `public_network`); on prodnxsr0009 the `[mds]` + `[mds.*]` sections and their blank-line separators are byte-identical to live. Rendering prodnxsr0009 with its current live `.9-.13` value reproduces the live file byte-for-byte, confirming the format match.
- RGW hosts (`roles::infra::ceph::rgw`, ausyd1nxvm2115-2119) include only `profiles::ceph::rgw` -> `profiles::ceph::conf`; they do not include `profiles::ceph::client`, so their variant is untouched and there is no duplicate `File[/etc/ceph/ceph.conf]`.

Rendered samples:

- osd-only (prodnxsr0008): `[global]` with all 19 `/32`s in `public_network`, nothing else.
- mon/mgr/mds (prodnxsr0009): the same `[global]` plus `[mds]` and `[mds.<host>-1|2]` for prodnxsr0009-0013, blank-line separated.

Reviewed-on: #515
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-09 00:00:20 +10:00
unkinben 82a5b2ee1a dns: stop exporting legacy DNS fragments (manage_export false) (#495)
## Why

During the k8s DNS cutover the `dns` module dual-writes every host's records two
ways: (a) LEGACY exported `@@concat::fragment`s collected onto the VM puppet DNS
master into `/var/named/sources/<zone>.conf`, and (b) NEW nsupdate/RFC2136 to the
k8s bind-authoritative cluster (`manage_nsupdate`). Both were gated on so the
legacy path stayed as a safety net.

The legacy path is no longer in any client's resolution path:

- **#490** repointed `profiles::dns::base::nameservers` from the VM resolver
  anycast `198.18.19.16` to the k8s **bind-resolvers** LB `198.18.200.7`. Every
  puppet client now resolves there.
- The k8s bind-resolvers forward `unkin.net`, `main.unkin.net`, and all
  `18.198.in-addr.arpa` reverse zones to the k8s **bind-authoritative** cluster
  (`198.18.200.6`), which is populated purely by the nsupdate path — the same
  `profiles::dns::record` defines, `manage_nsupdate` branch.
- The VM master's assembled `/var/named/sources` files are therefore orphaned:
  nothing resolves against them anymore.

This flips `profiles::dns::updater::manage_export: false` to stop nodes exporting
the legacy fragments. `manage_nsupdate` stays on.

## What changes

- `hieradata/common.yaml`: `profiles::dns::updater::manage_export` `true` -> `false`.

## Verification evidence

Pulled all **498** exported `Concat::Fragment` resources from PuppetDB (239 A,
232 PTR, 27 CNAME; 490 unique fqdn/type/value) and `dig`'d each against the k8s
authoritative read endpoint `198.18.200.7` (the dynamic path's serving side):

| Result | Count |
|---|---|
| MATCH | 447 |
| MISSING-from-dynamic | 38 |
| DIFFERENT-value | 5 |

Both non-match buckets are accounted for and are **not coverage gaps**:

- **38 MISSING** are all records the dynamic path deliberately filters via
  `profiles::dns::updater::deny_ranges`
  (`198.18.200.0/24`, `10.42.0.0/16`, ...) plus reverse zones the authoritative
  cluster isn't even authoritative for: `*-kube-lb0` (198.18.200.x),
  `*-flannel.1` (10.42.x.x pod CIDR), and `*-nomad`/`*-brcom1`
  (172.26.64.x / 10.255.255.x). These are exactly the k8s/LB noise the deny
  ranges exist to keep out of the authoritative zones. Verified every one falls
  in a denied/non-authoritative range.
- **5 DIFFERENT** are stale legacy duplicates that the dynamic path holds *more*
  correctly:
  - 4x `au-syd1-prod-halb.main.unkin.net` A: four ceph nodes (2069-2073) each
    self-register the shared name with their own primary IP (haproxy::dns
    exports `networking.ip` per host, a legacy collision). Dynamic resolves it to
    the single VIP `198.18.24.60`.
  - 1x `dashboard.ceph.unkin.net` CNAME: the ceph node self-publishes this to
    `lb1.unkin.net.` via its own nsupdate (per the k8s zones.yaml note); the
    exported VRRP-name fragment is stale.

Conclusion: the dynamic path fully covers the served record set, and the serving
path no longer depends on the exported files.

## Collector-behavior note (what the flip actually does)

`profiles::dns::zone` realizes the fragments with an exported-resource collector
(`Concat::Fragment <<| target == ... |>>`). Turning off export purges the
fragments from PuppetDB, so on the VM master concat rebuilds each
`/var/named/sources/<zone>.conf` down to just the header (SOA+NS+glue) as nodes
converge. That is **cosmetic**: no client resolves against the VM master anymore
(#490), and the k8s authoritative zones are unaffected (fed by nsupdate, not
these files).

## Rollback

Set `profiles::dns::updater::manage_export: true` again and let puppet converge;
nodes re-export their fragments and the VM-master zone files repopulate.

## Follow-up (after soak)

Deleting the `@@concat::fragment` / `Concat::Fragment <<||>>` collector code in
`profiles::dns::record` / `profiles::dns::zone` (and the VM `profiles::dns::master`
zone assembly) is a separate cleanup PR once this has soaked and the VM master is
confirmed decommissionable.

Reviewed-on: #495
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-25 12:29:21 +10:00
benvin e8443ccfa1 feat: migrate clients to use bind-dns in kubernetes (#490)
- change the default anycast nameserver to bind-internal resolvers in
  kubernetes

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #490
2026-07-20 22:41:29 +10:00
unkinben 88fcb97ad1 dns: nsupdate host records to the authoritative server (#475)
Replaces the exported-resources → puppet DNS master zone-file flow with per-host RFC2136 dynamic updates against the k8s **bind-authoritative** write endpoint (198.18.200.9). The master no longer manages zone files.

## Design
Each node assembles its DNS records into a local concat file; a systemd `.path` unit watches it and runs `dns-update` (nsupdate) on change — exactly the watch-a-file model requested.

## Changes
- **profiles::dns::updater** (new): concat records file + TSIG key file + `dns-update` script + `dns-update.service` (oneshot) + `dns-update.path` (watcher). The script sends only the delta since last run and deletes removed records, grouped per zone.
- **profiles::dns::record**: writes a local concat fragment (`zone|name|type|ttl|value`) instead of exporting `@@concat::fragment` to the master.
- **profiles::dns::base**: includes `profiles::dns::updater` (all nodes).
- **hiera**: `profiles::dns::updater` server/key_name/algorithm in common.yaml.

## Inert until keyed
The updater does nothing until `profiles::dns::updater::key_secret` (TSIG) is set in eyaml — records are assembled but not applied, so nodes are safe before the key exists.

## Prerequisites (k8s side, separate)
1. The `bind-authoritative` zones must set `dynamicUpdate: true` + an `updateKeyRef` (a client-update BindTSIGKey) so they accept these updates.
2. The TSIG key must be shared: the operator-generated key value goes into eyaml here (or the planned Vault-sync feature bridges it).

## Validated
puppet parser/epp validate, puppet-lint, and a functional test of the generated per-zone nsupdate message (replace + delete-removed).

Reviewed-on: #475
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-12 22:23:18 +10:00
unkinben 1e7dfb9d9d feat: manage additional ceph sections (#419)
- ensure mons configuration are managed in code
- ensure radowgw configuration are managed in code

Reviewed-on: #419
2025-11-08 19:19:44 +11:00
unkinben 9dd74013ea feat: create stalwart module (#418)
- add stalwart module
- add psql database on the shared patroni instance
- add ceph-rgw credentials to eyaml
- ensure psql pass and s3 access key are converted to sensitive

Reviewed-on: #418
2025-11-08 19:09:30 +11:00
unkinben 78adef0eee refactor: recreate profiles::postfix::gateway with parameterization and templates (#416)
- refactor profiles::postfix::gateway as parameterized class
- move base postfix parameters, transports, and virtuals to hiera for flexibility
- convert SMTP restrictions to arrays for better readability using join()
- add postscreen enable/disable boolean with conditional master.cf configuration
- add per-domain TLS policy maps (smtp_tls_policy_maps)
- convert alias_maps to array parameter for flexibility
- convert all postfix map files to ERB templates with parameter hashes
- add map parameters: sender_canonical_maps, sender_access_maps, relay_recipients_maps,
  relay_domains_maps, recipient_canonical_maps, recipient_access_maps, postscreen_access_maps, helo_access_maps
- move default map data to hiera while keeping parameters as empty hashes by default

This approach balances flexibility with data-driven configuration, allowing
easy customization through parameters while keeping transport/virtual maps
and default map data in hiera for role-specific overrides.

Reviewed-on: #416
2025-11-01 17:26:00 +11:00
unkinben 98b866fce7 feat: migrate puppet-agent to openvox (#408)
- change from puppet-agent to openvox-agent
- upgrade version from 7.34 to 7.36
- ensure workflow of: Yumrepo -> dnf-makecache -> Package

Reviewed-on: #408
2025-10-18 19:11:38 +11:00
unkinben 6e4bc9fbc7 feat: adding rke2 (#394)
- manage rke2 repos
- add rke2 module (init, params, install, config, service)
- split roles::infra::k8s::node -> control/compute roles
- moved common k8s config into k8s.yaml
- add bootstrap_node, manage server and token fields in rke2 config
- manage install of helm
- manage node attributes (from puppet facts)
- manage frr exclusions for service/cluster network

Reviewed-on: #394
2025-09-14 13:27:49 +10:00
unkinben 92728047e7 feat: add ceph rgw (#380)
- start managing ceph configuration file
- manage ceph-radosgw
- merge the ceph::conf and ceph::node profiles
- ensure the ceph repos exist
- mange nginx frontend and consul service

Reviewed-on: #380
2025-08-13 12:33:41 +10:00
unkinben ac36d9627b feat: capture all journald logs (#377)
- create module class for journald clients
- ensure module class it used on all hosts
- use consul service address for insert/journald

Reviewed-on: #377
2025-08-09 15:11:47 +10:00
unkinben fd902c1437 feat: create exporters module (#364)
- upgrade node_exporter, bring managed under exporters module
- upgrade postgres_exporter, bring managed under exporters module
- add flag to cleanup previous iterations of exporters from prometheus module
- fix issues with vmclusster: replication + dedup

Reviewed-on: #364
2025-07-27 13:28:41 +10:00
unkinben be02d3d150 feat: migrate to external ntp (#349)
- removing ntp vms from proxmox
- redirect ntp to external time sources

Reviewed-on: #349
2025-07-07 20:27:02 +10:00
unkinben 1d23fef82e feat: update settings for ceph (#298)
- enable root logins via ssh with keys
- add ssh key for ceph to root user

Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/298
2025-05-25 20:22:00 +10:00
unkinben d9e8637ad6 feat: manage more ceph requirements (#288)
- add ceph-common to provide utilities for managing ceph
- add root and sysadmin ssh keys for ceph deployments

Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/288
2025-05-17 11:14:45 +10:00
unkinben 3e0141bb1b feat: change to anycast resolver (#280)
Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/280
2025-05-11 11:39:00 +10:00
unkinben 9359b8902e feat: vault mlock (#266)
- enable mlock by default
- disable mlock on lxd/incus nodes (lxc doesnt support it)

Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/266
2025-04-26 22:43:20 +10:00
unkinben 496ed12a58 feat: change vault to use package install (#264)
- vault 18.2 rpm produced by rpmbuilder repo
- ensure the /etc/vault directory is managed
- ensure service file is managed by puppet
- ensure package comes from unkin repo (not hashicorp)
- disable_mlock as unprivileged containers cannot use mlock

Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/264
2025-04-26 18:40:31 +10:00
unkinben 9dc88e6db6 feat: deep merge zpools/datasets (#243)
- change prodnxsr0009 to use nvme0n1 as zfs device

Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/243
2025-04-02 20:35:04 +11:00
unkinben 829b1b05fd feat: cleanup consul from url install (#239)
- set bind_dir to be /usr/bin for rhel, /usr/local/bin for debian
- remove url-installed consul from rhel

Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/239
2025-03-30 18:40:09 +11:00
unkinben e3046563a2 chore: install consul from package (#233)
- upgrade to puppet-consul changed default install method to archive
- ensure package method is used
- dont manage the repo, consul is packaged by rpmbuilder

Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/233
2025-03-30 02:04:13 +11:00
unkinben dd5a4646ff feat: update all modules (#228)
- update puppetlabs-* modules
- update puppet-* modules
- add limits and sysctl

Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/228
2025-03-30 00:51:49 +11:00
unkinben afd3405c98 feat: add etcd module/role (#215)
- add etcd module
- add etcd role, profile and hieradata

Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/215
2025-01-26 20:00:20 +11:00
unkinben 35834f8f5a feat: add patroni/psql cluster
- add patroni puppet module
- add patroni role and hieradata
- add sql/patroni class that utilised consul
2024-08-10 22:34:43 +10:00
unkinben c846cc4e21 feat: add rundeck runner user
- add rundeck account on all hosts except rundeck
- add rundeck ssh private/public key to rundeck server
2024-08-06 22:33:32 +10:00
unkinben 6a3123e12e Merge pull request 'feat: change packages to Hash' (#120) from neoloc/packages_hash into develop
Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/120
2024-07-27 16:29:48 +10:00
unkinben cc01259a64 feat: change packages to Hash
- change from multiple arrays for managing packages to a hash
- change to ensure_packages to prevent duplicate resource conflicts
2024-07-27 13:01:06 +10:00
unkinben 946922fdb9 feat: add vrrp to halb
- update keepalived module to 5.1.0
- add keepalived::vrrp::* to be deep merged in hiera
- add vrrp dns configuration
- add vrrp instance/script to halb in syd1
2024-07-13 20:15:13 +10:00
unkinben bd5164fed3 feat: certbot reorg
- moved certbot into its own module
- added fact to list available certificates
- created systemd timer to rsync data to $data_dir/pub
- ensure the $data_dir/pub exists
- manage selinux for nginx
2024-07-08 22:33:11 +10:00
unkinben 2ab2cd1399 feat: deploy ldap-auth to all *arrs
- refactor sonarr locations to generalised locations
- set locations to be deep merged
- updated hiera_include statements for media and media subroles
- added eyaml entries for all ldap credentials
2024-07-06 22:50:10 +10:00
unkinben 6839fb8c5f feat: networking defaults
- add interface/route defaults
- merge defaults into each interface/route
2024-06-23 17:34:23 +10:00
unkinben 803a0ac01d Merge pull request 'neoloc/cephfs' (#54) from neoloc/cephfs into develop
Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/54
2024-06-23 15:34:04 +10:00
unkinben 5631f07e6e feat: add cephfs shared volume define
- add ceph class to manage ceph client configuration/packages
- add cephfs define for mounting volumes
- add ceph keyring define to manage secrets used to mount cephfs
2024-06-23 15:33:33 +10:00
unkinben 2d3f4414b7 fix: unar package not available on debian 2024-06-22 00:47:36 +10:00
unkinben 8548ef0284 Merge pull request 'neoloc/sonarr_deploy' (#48) from neoloc/sonarr_deploy into develop
Reviewed-on: https://git.query.consul/unkinben/puppet-prod/pulls/48
2024-06-21 22:53:06 +10:00
unkinben 681f9e3eb8 feat: deploy sonarr
- add required hieradata/role data to deploy sonarr
- add nginx simpleproxy
- add consul service/query
- add vault certificates
2024-06-21 22:51:40 +10:00
unkinben 62cac63f11 feat: add database generation to grafana
- ensure a database, user and credential is created for each grafana node
- ensure all databases for a region are included in a mariadb cluster
- refine params with stdlib types
2024-06-16 18:49:59 +10:00
unkinben 2b36ee3efa fix: proxmox root ssh
- allow proxmox hosts to accept root logins
2024-06-10 12:07:08 +10:00
unkinben 965e334636 Merge branch 'develop' into neoloc/sshsign_hostkeys 2024-06-09 20:39:27 +10:00
unkinben d4163233f6 Merge branch 'develop' into neoloc/sshsign_hostkeys 2024-06-09 20:38:25 +10:00
unkinben 52b06dcd8e feat: manage ssh known hosts
- disable use of stored configs for ssh-known-hosts
- manage the /etc/ssh/ssh_known_hosts content
2024-06-09 20:26:34 +10:00
unkinben 934f4be03c fix: dont manage loopback
- dont manage the lo interface
- cleanup /etc/hosts records
2024-06-09 09:06:54 +10:00
unkinben 777fe1aef6 feat: manage ssh server
- add ssh module
- include the ssh::server class
- manage sshd settings
2024-06-08 17:20:56 +10:00
unkinben 06545c6298 feat: change hiera_include, hiera_exclude
- change hiera_classes to hiera_include
- add method to remove classes from hiera_include through hiera_exclude
2024-06-08 17:07:58 +10:00
unkinben 51eeb13793 feat: add networking module
- manage interfaces and routes
- set default params for hosts
- add params class to networking module
- set defaults for debian
2024-06-08 17:07:51 +10:00
unkinben ad4f9b81f4 Merge pull request 'neoloc/syd1_certmanager_approle' (#224) from neoloc/syd1_certmanager_approle into develop
Reviewed-on: unkinben/puppet-prod#224
2024-05-26 00:38:16 +09:30