Add deliberate update playbook and unattended-upgrades role
Regular playbooks now use state: present, so they no longer upgrade packages as a side effect. This adds two separate, explicit mechanisms to keep the fleet patched instead: - update-packages.yml: ad hoc / to-be-scheduled fleet-wide upgrade (safe by default, dist available via -e), plus update-packages-pinned-example.yml as a template for pinning or bumping a single package outside that. - roles/unattended_upgrades: automatic security-only patching via unattended-upgrades, with a scheduled reboot window and mail left disabled pending a configured MTA. Applied to every host in full.yml and server.yml. Also removes a leftover `upgrade: yes` apt task from pi_standard_setup and server_basic_environment that was still doing a full upgrade on every routine run, defeating the point of the state: present switch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
32
README.md
32
README.md
@@ -12,6 +12,9 @@ Pis (audio players, sensors, music mouse, etc.) plus one home server.
|
|||||||
runs kept around for specific hosts or one-off tasks.
|
runs kept around for specific hosts or one-off tasks.
|
||||||
- `roles/` — one role per piece of functionality (audio backends, sensors,
|
- `roles/` — one role per piece of functionality (audio backends, sensors,
|
||||||
bluetooth monitoring, server basics, etc.). Each has a short `README.md`.
|
bluetooth monitoring, server basics, etc.). Each has a short `README.md`.
|
||||||
|
- `update-packages.yml` — deliberate, fleet-wide package update (see
|
||||||
|
"Keeping packages up to date" below). `update-packages-pinned-example.yml`
|
||||||
|
is a template for pinning or bumping a single package outside that.
|
||||||
- `lookup_plugins/keepass.py` — custom lookup plugin that fetches secrets
|
- `lookup_plugins/keepass.py` — custom lookup plugin that fetches secrets
|
||||||
(device passwords, wifi passphrase) from a running KeePassXC instance via
|
(device passwords, wifi passphrase) from a running KeePassXC instance via
|
||||||
its browser-integration protocol, instead of storing them in the repo.
|
its browser-integration protocol, instead of storing them in the repo.
|
||||||
@@ -43,3 +46,32 @@ ansible-playbook full.yml --limit <host>
|
|||||||
|
|
||||||
`ansible.cfg` points Ansible at `inventory.yml` and `roles/` by default, so
|
`ansible.cfg` points Ansible at `inventory.yml` and `roles/` by default, so
|
||||||
no extra flags are needed for those.
|
no extra flags are needed for those.
|
||||||
|
|
||||||
|
## Keeping packages up to date
|
||||||
|
|
||||||
|
Regular playbook runs (`full.yml`, `server.yml`, etc.) use `state: present`
|
||||||
|
for packages, so they only install what's missing — they never upgrade
|
||||||
|
anything as a side effect of an unrelated config change. Two separate,
|
||||||
|
deliberate mechanisms handle upgrades instead:
|
||||||
|
|
||||||
|
**Security patches — automatic.** The `unattended_upgrades` role (applied
|
||||||
|
to every host in `full.yml`/`server.yml`) configures `unattended-upgrades`
|
||||||
|
to install security-origin updates automatically, with a scheduled reboot
|
||||||
|
window (default 03:00, see `roles/unattended_upgrades/defaults/main.yml`)
|
||||||
|
for patches that need one. Not scoped to full dist-upgrades.
|
||||||
|
|
||||||
|
**Everything else — deliberate, ad hoc.**
|
||||||
|
```
|
||||||
|
just update <host_or_group> # e.g. `just update kitchenpi` or `just update`
|
||||||
|
venv/bin/ansible-playbook update-packages.yml --limit <host> --check --diff # dry run first
|
||||||
|
```
|
||||||
|
Defaults to `upgrade: safe` (upgrades in place, never installs/removes
|
||||||
|
packages to resolve dependencies — see the comment header in
|
||||||
|
`update-packages.yml` for the tradeoff against `dist`). There is no cron/
|
||||||
|
schedule wired up for this yet; run it ad hoc when you want the fleet
|
||||||
|
updated, or add a crontab entry yourself (a starting point is documented in
|
||||||
|
`update-packages.yml`'s header).
|
||||||
|
|
||||||
|
To pin a package to an exact version, or deliberately bump one named
|
||||||
|
package to latest outside this schedule, copy the pattern in
|
||||||
|
`update-packages-pinned-example.yml`.
|
||||||
|
|||||||
4
full.yml
4
full.yml
@@ -11,6 +11,7 @@
|
|||||||
- name: Musikserver Wohnzimmer oben
|
- name: Musikserver Wohnzimmer oben
|
||||||
hosts: musikserverwohnzimmeroben
|
hosts: musikserverwohnzimmeroben
|
||||||
roles:
|
roles:
|
||||||
|
- unattended_upgrades
|
||||||
- pi_standard_setup
|
- pi_standard_setup
|
||||||
- pi_hifiberry_amp
|
- pi_hifiberry_amp
|
||||||
- pi_squeezelite_custom
|
- pi_squeezelite_custom
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
- name: Kitchen pi
|
- name: Kitchen pi
|
||||||
hosts: kitchenpi
|
hosts: kitchenpi
|
||||||
roles:
|
roles:
|
||||||
|
- unattended_upgrades
|
||||||
- pi_standard_setup
|
- pi_standard_setup
|
||||||
- pi_hifiberry_amp
|
- pi_hifiberry_amp
|
||||||
- pi_squeezelite_custom
|
- pi_squeezelite_custom
|
||||||
@@ -34,6 +36,7 @@
|
|||||||
- name: Bedroom pi
|
- name: Bedroom pi
|
||||||
hosts: bedroompi
|
hosts: bedroompi
|
||||||
roles:
|
roles:
|
||||||
|
- unattended_upgrades
|
||||||
- pi_standard_setup
|
- pi_standard_setup
|
||||||
- pi_squeezelite_custom
|
- pi_squeezelite_custom
|
||||||
- pi_shairport
|
- pi_shairport
|
||||||
@@ -45,6 +48,7 @@
|
|||||||
- name: Musicmouse
|
- name: Musicmouse
|
||||||
hosts: musicmouse
|
hosts: musicmouse
|
||||||
roles:
|
roles:
|
||||||
|
- unattended_upgrades
|
||||||
- pi_standard_setup
|
- pi_standard_setup
|
||||||
- pi_hifiberry_amp
|
- pi_hifiberry_amp
|
||||||
- pi_musicmouse
|
- pi_musicmouse
|
||||||
|
|||||||
4
justfile
4
justfile
@@ -42,6 +42,10 @@ full limit="all":
|
|||||||
server:
|
server:
|
||||||
just run server.yml server
|
just run server.yml server
|
||||||
|
|
||||||
|
# Run the deliberate fleet package-update playbook, e.g. `just update kitchenpi`
|
||||||
|
update limit="all":
|
||||||
|
just run update-packages.yml {{limit}}
|
||||||
|
|
||||||
# List installed Galaxy collections
|
# List installed Galaxy collections
|
||||||
collections:
|
collections:
|
||||||
{{venv_bin}}/ansible-galaxy collection list
|
{{venv_bin}}/ansible-galaxy collection list
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
- name: Do apt update/upgrade
|
- name: Refresh apt cache
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
upgrade: "yes"
|
|
||||||
update_cache: "yes"
|
update_cache: "yes"
|
||||||
cache_valid_time: "7200"
|
cache_valid_time: "7200"
|
||||||
- name: Detect Raspi Model
|
- name: Detect Raspi Model
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
- name: Do apt update/upgrade
|
- name: Refresh apt cache
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
upgrade: "yes"
|
|
||||||
update_cache: "yes"
|
update_cache: "yes"
|
||||||
cache_valid_time: "300"
|
cache_valid_time: "300"
|
||||||
- name: Apt install download & unzip packages
|
- name: Apt install download & unzip packages
|
||||||
|
|||||||
9
roles/unattended_upgrades/README.md
Normal file
9
roles/unattended_upgrades/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# unattended_upgrades
|
||||||
|
|
||||||
|
Installs and configures `unattended-upgrades` for automatic security
|
||||||
|
patching, separate from `update-packages.yml` (which handles deliberate,
|
||||||
|
scheduled full-package updates — see the repo README). Security-only by
|
||||||
|
default. Reboots when required, at a fixed scheduled time (default 03:00),
|
||||||
|
rather than never or immediately — see `defaults/main.yml` to change this.
|
||||||
|
Mail-on-failure is supported but disabled by default since no mail
|
||||||
|
transport is configured on these hosts yet.
|
||||||
21
roles/unattended_upgrades/defaults/main.yml
Normal file
21
roles/unattended_upgrades/defaults/main.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
# Restrict to security-origin updates only (not a full dist-upgrade).
|
||||||
|
unattended_upgrades_security_only: true
|
||||||
|
|
||||||
|
# Extra Origins-Pattern lines appended verbatim, for when
|
||||||
|
# unattended_upgrades_security_only is turned off later (e.g. to also allow
|
||||||
|
# the "-updates" pocket). Left empty by default.
|
||||||
|
unattended_upgrades_origins_extra: []
|
||||||
|
|
||||||
|
# Reboot handling. Off by default would mean patches needing a reboot never
|
||||||
|
# take effect until someone reboots manually; scheduled means an automatic
|
||||||
|
# reboot at a fixed, low-traffic time on the days it's actually needed.
|
||||||
|
unattended_upgrades_auto_reboot: true
|
||||||
|
unattended_upgrades_auto_reboot_time: "03:00"
|
||||||
|
|
||||||
|
unattended_upgrades_remove_unused_deps: true
|
||||||
|
|
||||||
|
# Set to a mail address (and ensure a working mail transport is configured
|
||||||
|
# on the host) to get notified on failure. Empty disables mail entirely.
|
||||||
|
unattended_upgrades_mail_to: ""
|
||||||
|
unattended_upgrades_mail_on_only_error: true
|
||||||
31
roles/unattended_upgrades/tasks/main.yml
Normal file
31
roles/unattended_upgrades/tasks/main.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
- name: Install unattended-upgrades
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: unattended-upgrades
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Configure unattended-upgrades behavior
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: 50unattended-upgrades.j2
|
||||||
|
dest: /etc/apt/apt.conf.d/50unattended-upgrades
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Enable periodic apt update / unattended-upgrade timers
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: 20auto-upgrades.j2
|
||||||
|
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Ensure apt daily timers are enabled and running
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: "{{ item }}"
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
loop:
|
||||||
|
- apt-daily.timer
|
||||||
|
- apt-daily-upgrade.timer
|
||||||
7
roles/unattended_upgrades/templates/20auto-upgrades.j2
Normal file
7
roles/unattended_upgrades/templates/20auto-upgrades.j2
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// Managed by Ansible (roles/unattended_upgrades) — changes will be overwritten.
|
||||||
|
|
||||||
|
APT::Periodic::Update-Package-Lists "1";
|
||||||
|
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||||
|
APT::Periodic::AutocleanInterval "7";
|
||||||
|
APT::Periodic::Unattended-Upgrade "1";
|
||||||
|
APT::Periodic::Verbose "1";
|
||||||
29
roles/unattended_upgrades/templates/50unattended-upgrades.j2
Normal file
29
roles/unattended_upgrades/templates/50unattended-upgrades.j2
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Managed by Ansible (roles/unattended_upgrades) — changes will be overwritten.
|
||||||
|
|
||||||
|
Unattended-Upgrade::Origins-Pattern {
|
||||||
|
{% if unattended_upgrades_security_only %}
|
||||||
|
"origin=${distro_id},codename=${distro_codename},label=${distro_id}-Security";
|
||||||
|
"origin=${distro_id},codename=${distro_codename}-security,label=${distro_id}-Security";
|
||||||
|
"origin=Debian,codename=${distro_codename},label=Debian-Security";
|
||||||
|
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
|
||||||
|
{% else %}
|
||||||
|
"origin=${distro_id},codename=${distro_codename}";
|
||||||
|
"origin=${distro_id},codename=${distro_codename}-security";
|
||||||
|
"origin=${distro_id},codename=${distro_codename}-updates";
|
||||||
|
{% endif %}
|
||||||
|
{% for origin in unattended_upgrades_origins_extra %}
|
||||||
|
"{{ origin }}";
|
||||||
|
{% endfor %}
|
||||||
|
};
|
||||||
|
|
||||||
|
Unattended-Upgrade::Remove-Unused-Dependencies "{{ unattended_upgrades_remove_unused_deps | lower }}";
|
||||||
|
|
||||||
|
Unattended-Upgrade::Automatic-Reboot "{{ unattended_upgrades_auto_reboot | lower }}";
|
||||||
|
Unattended-Upgrade::Automatic-Reboot-Time "{{ unattended_upgrades_auto_reboot_time }}";
|
||||||
|
|
||||||
|
Unattended-Upgrade::SyslogEnable "true";
|
||||||
|
|
||||||
|
{% if unattended_upgrades_mail_to %}
|
||||||
|
Unattended-Upgrade::Mail "{{ unattended_upgrades_mail_to }}";
|
||||||
|
Unattended-Upgrade::MailOnlyOnError "{{ unattended_upgrades_mail_on_only_error | lower }}";
|
||||||
|
{% endif %}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
- name: Server
|
- name: Server
|
||||||
hosts: server
|
hosts: server
|
||||||
roles:
|
roles:
|
||||||
|
- unattended_upgrades
|
||||||
- server_basic_environment
|
- server_basic_environment
|
||||||
- server_debian_docker
|
- server_debian_docker
|
||||||
- server_exthdd_mount
|
- server_exthdd_mount
|
||||||
|
|||||||
29
update-packages-pinned-example.yml
Normal file
29
update-packages-pinned-example.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
# Template, not run on any schedule. Copy this when a package needs a
|
||||||
|
# controlled bump outside update-packages.yml — e.g. a security fix you
|
||||||
|
# want before the next scheduled run, or a version you deliberately don't
|
||||||
|
# want update-packages.yml's `safe` upgrade to move past.
|
||||||
|
#
|
||||||
|
# Run explicitly against the hosts that need it:
|
||||||
|
# venv/bin/ansible-playbook update-packages-pinned-example.yml --limit <host>
|
||||||
|
|
||||||
|
- name: Pin a package to an exact version
|
||||||
|
hosts: all
|
||||||
|
tasks:
|
||||||
|
- name: Install nginx pinned to a specific version
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: "nginx=1.18.0-6.1+deb11u3"
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
# Pinning like this also protects the package from update-packages.yml's
|
||||||
|
# `safe`/`dist` upgrade: apt won't move a pinned-by-version install
|
||||||
|
# past the given version on a plain upgrade.
|
||||||
|
|
||||||
|
- name: Bump a single named package to latest, deliberately
|
||||||
|
hosts: all
|
||||||
|
tasks:
|
||||||
|
- name: Upgrade openssl to latest available
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: openssl
|
||||||
|
state: latest # noqa: package-latest - deliberate, scoped to one named package, run ad hoc
|
||||||
|
update_cache: true
|
||||||
50
update-packages.yml
Normal file
50
update-packages.yml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
# Deliberate, fleet-wide package update run. Decoupled on purpose from
|
||||||
|
# full.yml/server.yml (which use state: present and must stay idempotent/
|
||||||
|
# no-op on routine runs) — this is the one place packages actually get
|
||||||
|
# upgraded.
|
||||||
|
#
|
||||||
|
# Ad hoc run:
|
||||||
|
# just update <limit>
|
||||||
|
# venv/bin/ansible-playbook update-packages.yml --limit <host_or_group>
|
||||||
|
#
|
||||||
|
# Dry run first:
|
||||||
|
# venv/bin/ansible-playbook update-packages.yml --limit <host_or_group> --check --diff
|
||||||
|
#
|
||||||
|
# Scheduling: not automated yet. When you're ready, the simplest option is
|
||||||
|
# a crontab entry on whichever machine you normally run ansible-playbook
|
||||||
|
# from, e.g. weekly at 04:00 on Sundays:
|
||||||
|
# 0 4 * * 0 cd /path/to/this/repo && venv/bin/ansible-playbook update-packages.yml >> update.log 2>&1
|
||||||
|
# Adjust the schedule by editing that line (`crontab -e`). Note this only
|
||||||
|
# fires if that machine is on and networked at the scheduled time.
|
||||||
|
#
|
||||||
|
# upgrade type tradeoff (update_packages_upgrade_type, default "safe"):
|
||||||
|
# safe - upgrades packages in place, never installs/removes packages to
|
||||||
|
# resolve dependencies. Won't silently swap out a kernel
|
||||||
|
# meta-package or drop something. Recommended default for an
|
||||||
|
# unattended/fleet-wide run.
|
||||||
|
# dist - full upgrade, will install/remove packages as needed (kernel
|
||||||
|
# transitions, etc). More thorough, more surprising. Use
|
||||||
|
# deliberately: `-e update_packages_upgrade_type=dist`.
|
||||||
|
- name: Update packages across the fleet
|
||||||
|
hosts: all
|
||||||
|
vars:
|
||||||
|
update_packages_upgrade_type: safe
|
||||||
|
update_packages_autoremove: false
|
||||||
|
tasks:
|
||||||
|
- name: Update apt cache and upgrade packages
|
||||||
|
ansible.builtin.apt:
|
||||||
|
update_cache: true
|
||||||
|
upgrade: "{{ update_packages_upgrade_type }}"
|
||||||
|
autoremove: "{{ update_packages_autoremove }}"
|
||||||
|
register: update_packages_result
|
||||||
|
|
||||||
|
- name: Check whether a reboot is required after this upgrade
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: /var/run/reboot-required
|
||||||
|
register: update_packages_reboot_required
|
||||||
|
|
||||||
|
- name: Report reboot required
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Reboot required on {{ inventory_hostname }}"
|
||||||
|
when: update_packages_reboot_required.stat.exists
|
||||||
Reference in New Issue
Block a user