Commit Graph

537 Commits (e76bc2c36c7e347b254a387179c949eeef28cf1e)

Author SHA1 Message Date
Dustin db5d1fb91a unifi: Switch from nginx to Caddy
Mostly for built-in ACME support.
2025-03-16 17:17:00 -05:00
Dustin fbbe86c651 r/unifi: Do not deploy exporter
The _unifi_exporter_ has been broken since several versions of UniFi
Network ago.
2025-03-16 16:40:57 -05:00
Dustin db54b03aa8 r/unifi: Switching to custom container image
The _linuxserver.io_ image for UniFi Network is deprecated.  It sucked
anyway.  I've created a simple image based on Debian that installs the
_unifi_ package from the upstream apt repository.  This image doesn't
require running anything as _root_, so it doesn't need a user namespace.
2025-03-16 16:40:57 -05:00
Dustin c300dc1b6c chrony: Add role/PB for chrony
I continually struggle with machines' (physical and virtual, even the
Roku devices!) clocks getting out of sync.  I have been putting off
fixing this because I wanted to set up a Windows-compatible NTP server
(i.e. on the domain controllers, with Kerberos signing), but there's
really no reason to wait for that to fix the clocks on all the
non-Windows machines, especially since there are exactly 0 Windows
machines on the network right now.

The *chrony* role and corresponding `chrony.yml` playbook are generic,
configured via the `chrony_pools`, `chrony_servers`, and `chrony_allow`
variables.  The values for these variables will configure the firewall
to act as an NTP server, synchronizing with the NTP pool on the
Internet, while all other machines will synchronize with it.  This
allows machines on networks without Internet access to keep their clocks
in sync.
2025-03-16 16:37:19 -05:00
Dustin 164d86d646 r/postgresql-data: Manage users and databases
This role can ensure PostgreSQL users and databases are created for
applications that are not themselves managed by Ansible.  Notably, we
need to do this for anything deployed in Kubernetes that uses the
central database server.
2025-02-01 17:36:58 -06:00
Dustin 878a099752 r/kubelet: Ensure iscsi service is running
The _iscsi.socket_ unit gets enabled by default with the
_iscsi-initiator-utils_ package is installed, but it won't start
automatically until the next boot.  Without this service running,
Longhorn volumes will not be able to attach to the node, so we need to
explicitly ensure it is running before any workloads are assigned to the
node.
2025-01-31 19:01:27 -06:00
Dustin a9a6a30e59 r/{cri-o,kubelet}: Support versioned packages
Fedora 41 introduced versioned package names for Kubernetes components,
including CRI-O.  The intent is to allow multiple versions of Kubernetes
to be available (but not necessarily installed) within a given Fedora
release.  In order to use these packages, we need to set the desired
Kubernetes version, via the new `kubernetes_version` Ansible variable.
2025-01-31 18:57:21 -06:00
Dustin cbc4d29bd6 r/base: Install python3-libdnf5
Fedora 41 uses _dnf5_ by default.  Being written in C, its Python API is
an optional feature that needs to be installed separately.
2025-01-31 18:55:58 -06:00
Dustin a58dbb74c5 r/vmhost: Clean up qemu packages
At some point, the _qemu-kvm_ package became a meta-package that
installs _everything_ QEMU-related.  All drivers, backends, frontends,
etc. get pulled in, which results in a huge amount of wasted space.
Recently, the VM hosts started getting alerts about their `/` filesystem
getting too full, which is how I discovered this.

We can dramatically reduce the disk space footprint by installing only
the "core" package and the drivers we need for our servers.

After making and applying this change, which marks the listed packages
as "leaf" installs, I then manually uninstalled the _qemu-kvm_ package.
This uninstalled everything else that is not specifically listed.
2025-01-28 17:36:35 -06:00
Dustin 3e8ac36f88 r/vmagent: Rework as container deployment
Like the _blackbox-exporter_ role, the _vmagent_ role now deploys
`vmagent` as a container.  This simplifies the process considerably,
eliminating the download/transfer step.

While refactoring this role, I also changed how the trusted CA
certificates are handled.  Rather than copy files, the role now expects
a `vmagent_ca_certs` variable.  This variable is a mapping of
certificate name (file name without extension) to PEM contents.  This
allows certificates to be defined using normal host/group variables.
2025-01-26 13:08:59 -06:00
Dustin dcf1e5adfc r/blackbox-exporter: Rework to run as container
Instead of downloading the `blackbox_exporter` binary from GitHub and
copying it to the managed node, the _blackbox-exporter_ role now
installs _podman_ and configures a systemd container unit (Quadlet) to
run it in a container.  This simplifies the deployment considerably, and
will make updating easier (just run the playbook with `-e
blackbox_exporter_pull_image=true`).
2025-01-26 13:06:54 -06:00
Dustin e51e933661 r/gitea: Serve kickstarts over HTTP
I want to use Gita as the canonical source for Anaconda kickstart
scripts.  There are certain situations, however, where they cannot be
accessed via HTTPS, such as on a Raspberry Pi without an RTC, since it
cannot validate the certificate without the correct time.  Thus, the
web server must not force an HTTPS redirect for these, but serve them
directly.
2024-12-27 10:51:00 -06:00
Dustin a00ffd10df r/jellyfin: Fix system.xml template whitespace
Jellyfin is one of those stupid programs that thinks it needs to mutate
its own config.  At startup, it apparently reads `system.xml` and then
writes it back out.  When it does this, it trims the final newline from
the file.  Then, the next time Ansible runs, the template rewrites the
file with the trailing newline, and thus determines that the file has
changed and restarts the service.  This cycle has been going on for a
while and is rather annoying.
2024-12-12 06:36:23 -06:00
Dustin 15cb675297 r/kubelet: Pass --config arg to service
The systemd unit configuration installed by Fedora's _kubeadm_ package
does not pass the `--config` argument to the kubelet service.  Without
this argument, the kubelet will not read the configuration file
generated by `kubeadm` from the `kubelet-config` ConfigMap.  Thus,
various features will not work correctly, including server TLS
bootstrap.
2024-12-07 09:35:57 -06:00
Dustin 7a5f01f8a3 r/doas: Configure sudo alternative
In the spirit of replacing bloated tools with unnecessary functionality
with smaller, more focused alternatives, we can use `doas` instead of
`sudo`.  Originally, it was a BSD tool, but the Linux port supports PAM,
so we can still use `pam_auth_ssh_agent` for ppasswordless
authentication.
2024-11-24 10:33:21 -06:00
Dustin 0f600b9e6e kubernetes: Manage worker nodes
So far, I have been managing Kubernetes worker nodes with Fedora CoreOS
Ignition, but I have decided to move everything back to Fedora and
Ansible.  I like the idea of an immutable operating system, but the FCOS
implementation is not really what I want.  I like the automated updates,
but that can be accomplished with _dnf-automatic_.  I do _not_ like
giving up control of when to upgrade to the next Fedora release.
Mostly, I never did come up with a good way to manage application-level
configuration on FCOS machines.  None of my experiments (Cue+tmpl,
KCL+etcd+Luci) were successful, which mostly resulted in my manually
managing configuration on nodes individually.  Managing OS-level
configuration is also rather cumbersome, since it requires redeploying
the machine entirely.  Altogether, I just don't think FCOS fits with my
model of managing systems.

This commit introduces a new playbook, `kubernetes.yml`, and a handful of
new roles to manage Kubernetes worker nodes running Fedora Linux.  It
also adds two new deploy scripts, `k8s-worker.sh` and `k8s-longhorn.sh`,
which fully automate the process of bringing up worker nodes.
2024-11-24 10:33:21 -06:00
Dustin 164f3b5e0f r/wal-g-pg: Handle versioned storage locations
The target location for WAL archives and backups saved by WAL-G should
be separated based on the major version of PostgreSQL with which they
are compatible.  This will make it easier to restore those backups,
since they can only be restored into a cluster of the same version.

Unfortunately, WAL-G does not natively handle this.  In fact, it doesn't
really have any way of knowing the version of the PostgreSQL server it
is backing up, at least when it is uploading WAL archives.  Thus, we
have to include the version number in the target path (S3 prefix)
manually.  We can't rely on Ansible to do this, because there is no way
to ensure Ansible runs at the appropriate point during the upgrade
process.  As such, we need to be able to modify the target location as
part of the upgrade, without causing a conflict with Ansible the next
time it runs.

To that end, I've changed how the _wal-g-pg_ role creates the
configuration file for WAL-G.  Instead of rendering directly to
`wal-g.yml`, the role renders a template, `wal-g.yml.in`.  This template
can include a `@PGVERSION@` specifier.  The `wal-g-config` script will
then use `sed` to replace that specifier with the version of PostgreSQL
installed on the server, rendering the final `wal-g.yml`.  This script
is called both by Ansible in a handler after generating the template
configuration, and also as a post-upgrade action by the
`postgresql-upgrade` script.

I originally wanted the `wal-g-config` script to use the version of
PostgreSQL specified in the `PG_VERSION` file within the data directory.
This would ensure that WAL-G always uploads/downloads files for the
matching version.  Unfortunately, this introduced a dependency conflict:
the WAL-G configuration needs to be present before a backup can be
restored, but the data directory is empty until after the backup has
been restored.  Thus, we have to use the installed server version,
rather than the data directory version.  This leaves a small window
where WAL-G may be configured to point to the wrong target if the
`postgresql-upgrade` script fails and thus does not trigger regenerating
the configuration file.  This could result in new WAL archives/backups
being uploaded to the old target location.  These files would be
incompatible with the other files in that location, and could
potentially overwrite existing files.  This is rather unlikely, since
the PostgreSQL server will not start if the _postgresql-upgrade.service_
failed.  The only time it should be possible is if the upgrade fails in
such a way that it leaves an empty but valid data directory, and then
the machine is rebooted.
2024-11-17 10:27:31 -06:00
Dustin e861883627 r/pgsql-server-base: Add post-upgrade capability
The `postgresql-upgrade` script will now run any executables located in
the `/etc/postgresql/post-upgrade.d` directory.  This will allow making
arbitrary changes to the system after a PostgreSQL major version
upgrade.  Notably, we will use this capability to change the WAL-G
configuration to upload WAL archives and backups to the correct
version-specific location.
2024-11-17 10:27:31 -06:00
Dustin 965742d2b0 r/postgresql-server-base: Factor out prep steps
There's a bit of a dependency loop between the _postgresql-server_ role
and other roles that supplement it, like _wal-g-pg_ and
_postgresql-cert_.  The latter roles need PostgreSQL installed, but when
those roles are used, the server cannot be started until they have been
applied.  To resolve this situation, I've broken out the initial
installation steps from the _postgresql-server_ role into
_postgresql-server-base_.  Roles that need PostgreSQL installed, but
need to be applied before the server can start, can depend on this role.
2024-11-17 10:27:31 -06:00
Dustin 53b39338dd r/postgresql-server: Add script to upgrade database
The `postgresql-upgrade.sh` script arranges to run `pg_upgrade` after a
major PostgreSQL version update.  It's scheduled by a systemd unit,
_postgresql-upgrade.service_, which runs only after an OS update.
2024-11-17 10:27:31 -06:00
Dustin 0048a87630 r/postgresql-server: Set become on postgres tasks
Tasks that must run as the _postgres_ user need to explicity enable
`become`, in case it is not already enabled at the playbook level.  This
can happen, for example, when the playbook is running directly as root.
2024-11-16 11:50:28 -06:00
Dustin 6115762847 r/serterm: Deploy serial terminal multiplexer
Using `tmux`, we can spawn a bunch of `picocom` processes for the serial
ports connected to other server's console ports.  The
_serial-terminal-server_ service manages the `tmux` server process,
while the individual _serial-terminal-server-window@.service_ units
create a window in the `tmux` session.

The serial terminal server runs as a dedicated user.  The SSH server is
configured to force this user to connect to the `tmux` session.  This
should help ensure the serial consoles are accessible, even if the
Active Directory server is unavailable.
2024-11-10 13:15:08 -06:00
Dustin 8b9cf1985a r/wal-g-pg: Schedule weekly delete jobs
WAL-G slows down significantly when too many backups are kept.  We need
to periodically clean up old backups to maintain a reasonable level of
performance, and also keep from wasting space with useless old backups.
2024-11-05 19:28:57 -06:00
Dustin eaf9cbef9a Merge remote-tracking branch 'origin/frigate-exporter' 2024-11-05 07:01:31 -06:00
Dustin c1dc52ac29 Merge branch 'loki' 2024-11-05 07:01:13 -06:00
Dustin 39d9985fbd r/loki-caddy: Caddy reverse proxy for Loki
Caddy handles TLS termination for Loki, automatically requesting and
renewing its certificate via ACME.
2024-11-05 06:54:27 -06:00
Dustin 168bfee911 r/webites: Add apps.du5t1n.xyz F-Droid repo
I want to publish the _20125_ Status application to an F-Droid
repository to make it easy for Tabitha to install and update.  F-Droid
repositories are similar to other package repositories: a collection of
packages and some metadata files.  Although there is a fully-fledged
server-side software package that can manage F-Droid repositories, it's
not required: the metadata files can be pre-generated and then hosted by
a static web server just fine.

This commit adds configuration for the web server and reverse proxy to
host the F-Droid repository at _apps.du5t1n.xyz_.
2024-11-05 06:47:02 -06:00
Dustin 7e8aee072e r/bitwarden_rs: Redirect to canonical host name
Bitwarden has not worked correctly for clients using the non-canonical
domain name (i.e. _bitwarden.pyrocufflink.blue_) for quite some time.
This still trips me up occasionally, though, so hopefully adding a
server-side redirect will help.  Eventually, I'll probably remove the
non-canonical name entirely.
2024-11-05 06:37:03 -06:00
Dustin 0807afde57 r/dch-proxy: Use separate sockets for HTTP v4/v6
Although listening on only an IPv6 socket works fine for the HTTP
front-end, it results in HAProxy logging client requests as IPv4-mapped
IPv6 addresses.  For visual processing, this is ok, but it breaks Loki's
`ip` filter.
2024-11-05 06:34:55 -06:00
Dustin 90351ce59e r/dch-proxy: Include host name in log messages
When troubleshooting configuration or connection issues, it will be
helpful to have the value of the HTTP Host header present in log
messages emitted by HAProxy.  This will help reason about HAProxy's
routing decisions.

For TLS connections, of course, we don't have access to the Host header,
but we can use the value of the TLS SNI field.  Note that the requisite
`content set-var` directive MUST come before the `content accept`;
HAProxy stops processing all `tcp-request content ...` directives once
it has encountered a decision.
2024-11-05 06:32:49 -06:00
Dustin 3ca94d2bf4 r/haproxy: Enable Prometheus metrics
HAProxy can export stats in Prometheus format, but this requires
special configuration of a dedicated front-end.  To support this, the
_haproxy_ Ansible role now has a pair of variables,
`haproxy_enable_stats` and `haproxy_stats_port`, which control whether
or not the stats front-end is enabled, and if so, what port it listens
on.  Note that on Fedora with the default SELinux policy, the port must
be labelled either `http_port_t` or `http_cache_port_t`.
2024-11-05 06:23:49 -06:00
Dustin 9f30998fbf r/jellyfin: Enable Prometheus metrics
Jellyfin can expose metrics in Prometheus format, but this functionality
is disabled by default.  To enable it, we must set `EnableMetrics` in
the configuration file.  This commit adds a template configuration file
that uses the `jellyfin_enable_metrics` Ansible variable to control this
value.
2024-11-05 06:21:38 -06:00
Dustin ccf33f90e0 r/frigate-exporter: Deploy Prometheus exporter
Frigate exports useful statistics natively, but in a custom JSON format.
There is a [feature request][0] to add support for Prometheus format,
but it's mostly being ignored.  A community member has created a
standalone process that converts the JSON format into Prometheus format,
though, which we can use.

[0]: https://github.com/blakeblackshear/frigate/issues/2266
2024-10-21 20:27:31 -05:00
Dustin 4cd983d5f4 loki: Add role+playbook for Grafana Loki
The current Grafana Loki server, *loki0.pyrocufflink.blue*, runs Fedora
CoreOS and is managed by Ignition and *cfg*.  Since I have declared
*cfg* a failed experiment, I'm going to re-deploy Loki on a new VM
running Fedora Linux and managed by Ansible.

The *loki* role installs Podman and defines a systemd-managed container
to run Grafana Loki.
2024-10-20 12:10:55 -05:00
Dustin 388fd91096 r/nginx: Configure error/access syslog separately
There may be cases where we want either error logs or access logs to be
sent to syslog, but not both.  To support these, there are now two
variables: `nginx_access_log_syslog` and `nginx_error_log_syslog`.
Both use the value of the `nginx_log_syslog` variable by default, so
existing users of the _nginx_ role will continue to work as before.
2024-10-20 12:10:17 -05:00
Dustin 845911dcbd r/nginx: Make logging to files optional
If _nginx_ is configured to send error/access log messages to syslog, it
may not make sense to _also_ send messages to log files as well.  The
`nginx_error_log_file` and `nginx_access_log_file` variables are now
available to control whether/where to send log messages.  Setting either
of these to a falsy value will disable logging to a file.  A non-empty
string value is interpreted as the path to a log file.  By default, the
existing behavior of logging to `/var/log/nginx/error.log` and
`/var/log/nginx/access.log` is preserved.
2024-10-14 12:00:19 -05:00
Dustin 87b9014721 r/statsd-exporter: Deploy statsd exporter
The *statsd exporter* is a Prometheus exporter that converts statistics
from StatsD format into Prometheus metrics.  It is generally useful as a
bridge between processes that emit event-based statistics, turning them
into Prometheus counters and gauges.
2024-10-13 19:59:52 -05:00
Dustin a22c8aa0d2 r/nextcloud: Configure trashbin retention
Setting the `trashbin_retention_obligation` setting to `auto, 30` should
supposedly delete files in users' trash bins after 30 days.
2024-10-13 18:38:12 -05:00
Dustin 265aa074aa r/nextcloud: Configure Memories app
The [Memories] app for Nextcloud provides a better user interface and
more features than the built-in Photos app.  The latter seems to be
somewhat broken recently (timeline stops in June 2024, even though there
are more recent photos available), so we're trying out Memories (and
Recognize for facial recognition).

[Memories]: https://memories.gallery
2024-10-13 18:36:25 -05:00
Dustin 5ab0bcd5bf r/nextcloud: Update rewrite config for .mjs files
Nextcloud 28+ uses JavaScript modules (`.mjs` files).  These need to be
served from the filesystem like other static files, so the *mod_rewrite*
configuration needs to be updated as such.
2024-10-13 18:35:01 -05:00
Dustin 1e6ab546bc r/vmhost: Create directory for console logs
Need a directory where _libvirt_ can write logs from VM serial console
output.
2024-10-13 18:30:04 -05:00
Dustin 219fe75424 r/nginx: logrotate: do not delay compressing
_nginx_ access logs are typically either very small or very large.  For
small log files, it's fast enough to decompress them on the fly if
necessary.  For large files, they may take up so much space in
uncompressed form that the log volume fills too quickly.  In either
case, compressing the files as soon as they are rotated is a good
option, especially since their contents should already be sent to Loki.
2024-09-30 12:43:25 -05:00
Dustin 829c04332d r/nginx: Configure logrotate
The default `logrotate` configuration for _nginx_ may not be appropriate
for high-volume servers.  The `nginx_keep_num_logs` variable is now
available to control how many days of logs are kept.
2024-09-29 11:20:29 -05:00
Dustin 9e610eaf11 r/minio-backups-cert: Enable/start cerbot timer
Forgot to ensure the _certbot-renew.timer_ unit was enabled and started,
so the MinIO certificate did not get renewed the first time.
2024-09-08 09:15:36 -05:00
Dustin 7d93ba836e r/restic: Enhance restic-backup security sandbox
Since `restic` needs to run as root in order to back up files regardless
of their permissions, we need to restrict it to doing only that.  Using
systemd sandbox features, especially the capability bounding set, we can
remove all of _root_'s powers except the ability to read all files.
2024-09-04 17:43:24 -05:00
Dustin 0f4dea9007 restic: Add role+playbook for Restic backups
The `restic.yml` playbook applies the _restic_ role to hosts in the
_restic_ group.  The _restic_ role installs `restic` and creates a
systemd timer and service unit to run `restic backup` every day.

Restic doesn't really have a configuration file; all its settings are
controlled either by environment variables or command-line options. Some
options, such as the list of files to include in or exclude from
backups, take paths to files containing the values.  We can make use of
these to provide some configurability via Ansible variables.  The
`restic_env` variable is a map of environment variables and values to
set for `restic`.  The `restic_include` and `restic_exclude` variables
are lists of paths/patterns to include and exclude, respectively.
Finally, the `restic_password` variable contains the password to decrypt
the repository contents.  The password is written to a file and exposed
to the _restic-backup.service_ unit using [systemd credentials][0].

When using S3 or a compatible service for respository storage, Restic of
course needs authentication credentials.  These can be set using the
`restic_aws_credentials` variable.  If this variable is defined, it
should be a map containing the`aws_access_key_id` and
`aws_secret_access_key` keys, which will be written to an AWS shared
credentials file.  This file is then exposed to the
_restic-backup.service_ unit using [systemd credentials][0].

[0]: https://systemd.io/CREDENTIALS/
2024-09-04 09:40:29 -05:00
Dustin 6f9cd7e4af r/postgres-exporter: Do not connect to tempate1 DB
It turns out, having the exporter connect to the _template1_ database is
not a great idea.  PostgreSQL does not allow creating a new database if
the template database is currently being accessed by any clients.  Since
_template1_ is the default choice, the `createdb` command will probably
fail.

It doesn't specifically matter which database the exporter connects to,
since it reads most (all?) of its data from the PostgreSQL catalog,
which isn't database-specific.
2024-09-02 21:15:23 -05:00
Dustin 22dbc3ebc1 r/nextcloud-db-cert: Fetch client cert from k8s
Currently, the certificate authority that issues certificates for
PostgreSQL clients is hosted in Kubernetes and managed by
_cert-manager_.  Certificates it issues are stored in Kubernetes Secret
resources, making them easy to consume by applications running in the
cluster, but not for anything outside.  Since Nextcloud runs on its own
VM, we need a way to get the certificate out of the Secret and into a
file on that machine.  To that end, I've written the
`nextcloud-fetch-cert.py` script.  This script uses a Kubernetes Service
Account token to authenticate to the Kubernetes API and download the
contents of the Secret.  It runs periodically, triggered by a systemd
timer unit, to ensure the certificate is always up-to-date.

The obvious drawback to this approach is the requirement for a static
token.  Since there's not really a way to "renew" Service Account
tokens, it needs to be issued with a fairly long duration, to mitigate
the risk of being unable to fetch a new certificate once it has expired
because the token has also expired.  This somewhat negates the advantage
of using certificates for authentication, since now the machine needs a
static, pre-defined secret.

At some point, I may deploy another instance of _step-ca_ to manage the
PostgreSQL client CA.  Clients can then use e.g. `certbot` or `step ca
certificate` to obtain their certificates.  I chose not to implement
this yet, though for a couple of reasons.  First, I need to move the
Nextcloud database very soon, so we switch to using `restic` for backups
without having to deal with the database.  Second, I am still
considering moving Nextcloud into Kubernetes eventually, where it will
be able to get the Secret directly; since Nextcloud is the only client
outside the cluster, it may not be worth setting up _step-ca_ in that
case.
2024-09-02 20:35:32 -05:00
Dustin 924107abbe nextcloud: Support remote database server
The _nextcloud_ role originally handled setting up the PostgreSQL
database and assumed that it was running on the same server as Nextcloud
itself.  I have factored out those tasks into their own role,
_nextcloud-db_, which can be applied to a separate host.

I have also introduced some new variables (`nextcloud_db_host`,
`nextcloud_db_name`, `nextcloud_db_user`, and `nextcloud_db_password`),
which can be used to specify how to connect to the database, if it is
hosted remotely.  Since these variables are used by both the _nextcloud_
and _nextcloud-db_ roles, they are actually defined in a separate role,
_nextcloud-base_, upon which both depend.
2024-09-02 20:29:51 -05:00
Dustin 226232414f r/jellyfin: Fix HAProxy vhost
Without including the settings from `ssl.include`, the virtual host
bound to port 8443 expects to handle plain HTTP traffic, rather than
HTTPS.
2024-09-01 17:33:22 -05:00