668b63a398
pve-manager 8.4.11 -> 8.4.20, corosync 3.1.9 -> 3.1.10-pve2, and kernel
6.8.12-42 staged on the /boot LV. dpkg clean, nothing outstanding for
apt -f install, all PVE services active, cluster quorate, no unapplied
conffiles. Reboot deliberately deferred at operator request, so the host
still runs 6.8.12-13 until a chosen window.
This validates the GRUB fix from 061c4b7 under the exact condition it
was written for. update-grub regenerated entries for the new kernel and
entry 0 -- what GRUB_DEFAULT=0 selects -- is now
/vmlinuz-6.8.12-42-pve with root=ZFS=nvme/ROOT/pve-1, supplied by the
grub.d drop-in since grub-mkconfig cannot derive the pool name itself.
The old kernel keeps correct entries as a fallback and the ext4 rollback
entry is untouched. Had the fix not landed first, saved_entry would
still be pinned to 6.8.12-13 and the host would boot the old kernel
indefinitely -- 161 security updates installed and never run.
/boot holds both kernel sets at 176M used of 488M, confirming the 512M
LV carved out of swap was sized correctly.
Adds a ZFS snapshot step to the upgrade playbook, taken automatically on
ZFS-root nodes before any package lands. That is the first real use of
the boot-environment upside the migration was meant to unlock: rollback
for this upgrade is now `zfs rollback -r
nvme/ROOT/pve-1@pre-upgrade-20260818T141652Z && reboot` rather than
archaeology in dpkg. Also documents that the corosync bump restarts
corosync mid-upgrade, which on a 2-node cluster is a brief quorum event.
154 lines
6.6 KiB
YAML
154 lines
6.6 KiB
YAML
# Upgrade a Proxmox node's packages. DOES NOT REBOOT — reboot is a separate,
|
|
# deliberate step because it has cluster and NFS consequences this playbook
|
|
# cannot see.
|
|
#
|
|
# Run: scripts/elway root@<node> --playbook playbooks/pve-node-upgrade.yaml
|
|
#
|
|
# ⚠ Both ESH nodes are members of the 2-node `esh-pve-cluster` (quorum 2, no
|
|
# qdevice). Upgrading is safe while both are up; REBOOTING makes the survivor's
|
|
# /etc/pve read-only until the node returns. Do one node at a time and let the
|
|
# cluster go quorate again before touching the second. corosync 3.1.9 -> 3.1.10
|
|
# is a minor bump and rolling-safe, but do not leave the pair skewed longer than
|
|
# the window needs.
|
|
#
|
|
# ⚠ For esh-pve-nas specifically, run playbooks/esh-pve-nas-fix-grub-default.yaml
|
|
# FIRST. Its boot default used to pin a single kernel, so installing a new one
|
|
# would either break the default entry or silently keep booting the old kernel.
|
|
#
|
|
# ⚠ The corosync bump RESTARTS corosync mid-upgrade, which on a 2-node cluster is
|
|
# a brief quorum event — both nodes' /etc/pve go read-only for a few seconds and
|
|
# then recover. Guests are unaffected and PVE does this routinely, but do not run
|
|
# it concurrently with anything that writes cluster config, and check
|
|
# `pvecm status` afterwards rather than assuming.
|
|
#
|
|
# Conffile policy: --force-confdef + --force-confold, i.e. keep the on-disk
|
|
# version wherever a package ships a changed conffile. That is the right default
|
|
# for these hosts (hand-tuned /etc/default/grub, grub.d drop-ins, storage.cfg),
|
|
# and it means a genuinely important upstream conffile change will be left as a
|
|
# .dpkg-dist file rather than applied — the verify phase lists any that appear so
|
|
# they are not silently ignored.
|
|
|
|
vars:
|
|
backup_dir: /root/pre-upgrade-backup
|
|
|
|
steps:
|
|
- name: GUARD — cluster is quorate before we start
|
|
shell: |
|
|
pvecm status 2>/dev/null | grep -q "Quorate:.*Yes" || {
|
|
echo "cluster is NOT quorate — resolve that before upgrading"; exit 1; }
|
|
echo " quorate; nodes: $(pvecm nodes 2>/dev/null | awk 'NR>2 && $3 {print $3}' | tr '\n' ' ')"
|
|
changed_when: "false"
|
|
|
|
- name: GUARD — enough free space on / for the unpack
|
|
shell: |
|
|
avail=$(df -Pk / | awk 'NR==2{print $4}')
|
|
test "$avail" -gt 2097152 || { echo "less than 2G free on / — refusing"; exit 1; }
|
|
echo " / free: $(df -h / | awk 'NR==2{print $4}')"
|
|
if findmnt -no TARGET /boot >/dev/null 2>&1; then
|
|
bavail=$(df -Pk /boot | awk 'NR==2{print $4}')
|
|
test "$bavail" -gt 204800 || { echo "less than 200M free on /boot — refusing"; exit 1; }
|
|
echo " /boot free: $(df -h /boot | awk 'NR==2{print $4}')"
|
|
else
|
|
echo " /boot is part of / on this node"
|
|
fi
|
|
changed_when: "false"
|
|
|
|
- name: Snapshot the config that matters before touching packages
|
|
shell: |
|
|
mkdir -p {{ backup_dir }}
|
|
tar czf {{ backup_dir }}/pre-upgrade-$(hostname)-config.tar.gz \
|
|
-C / etc/pve etc/network/interfaces etc/fstab etc/default/grub \
|
|
etc/apt etc/corosync 2>/dev/null || true
|
|
dpkg -l > {{ backup_dir }}/dpkg-before.txt
|
|
pveversion -v > {{ backup_dir }}/pveversion-before.txt 2>&1
|
|
ls -la {{ backup_dir }}/
|
|
creates: "{{ backup_dir }}/pre-upgrade-backup.done"
|
|
|
|
# On a ZFS-root node this is the cheapest insurance available: an instant,
|
|
# space-free snapshot of the entire userspace before 200+ packages land. If the
|
|
# upgrade goes wrong, the recovery is a rollback and a reboot rather than an
|
|
# archaeology session in dpkg. Skipped automatically on non-ZFS roots.
|
|
- name: Snapshot the root dataset (ZFS-root nodes only)
|
|
shell: |
|
|
ds=$(findmnt -no SOURCE /)
|
|
snap="${ds}@pre-upgrade-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
zfs snapshot "$snap"
|
|
echo " created $snap"
|
|
echo " rollback if needed: zfs rollback -r $snap && reboot"
|
|
zfs list -t snapshot -o name,used,creation -s creation "$ds" 2>/dev/null | tail -4
|
|
when: "test \"$(findmnt -no FSTYPE /)\" = zfs"
|
|
|
|
- name: Refresh package lists
|
|
shell: apt-get update -qq
|
|
changed_when: "true"
|
|
|
|
- name: Record what is about to change
|
|
shell: |
|
|
apt-get -s dist-upgrade 2>/dev/null | grep -E "^Inst " > {{ backup_dir }}/planned-upgrade.txt
|
|
echo " $(wc -l < {{ backup_dir }}/planned-upgrade.txt) packages planned"
|
|
grep -E "kernel|corosync|pve-manager|zfs" {{ backup_dir }}/planned-upgrade.txt | sed 's/^/ /'
|
|
changed_when: "false"
|
|
|
|
- name: dist-upgrade
|
|
shell: |
|
|
DEBIAN_FRONTEND=noninteractive apt-get -y \
|
|
-o Dpkg::Options::=--force-confdef \
|
|
-o Dpkg::Options::=--force-confold \
|
|
dist-upgrade 2>&1 | tail -30
|
|
changed_when: "true"
|
|
|
|
- name: Record the result
|
|
shell: |
|
|
pveversion -v > {{ backup_dir }}/pveversion-after.txt 2>&1
|
|
head -3 {{ backup_dir }}/pveversion-after.txt
|
|
touch {{ backup_dir }}/pre-upgrade-backup.done
|
|
changed_when: "true"
|
|
|
|
verify:
|
|
- name: dpkg is in a clean state
|
|
shell: |
|
|
broken=$(dpkg -l | grep -cE "^i[^i]| ^r" || true)
|
|
dpkg --audit 2>&1 | head -5
|
|
test -z "$(dpkg --audit 2>/dev/null)" || { echo "dpkg --audit is not clean"; exit 1; }
|
|
echo "dpkg clean"
|
|
changed_when: "false"
|
|
|
|
- name: No packages left half-configured
|
|
shell: |
|
|
n=$(apt-get -s -f install 2>/dev/null | grep -cE "^Inst |^Conf " || true)
|
|
test "$n" -eq 0 || { echo "apt -f install wants to do $n things"; exit 1; }
|
|
echo "nothing outstanding for apt -f install"
|
|
changed_when: "false"
|
|
|
|
- name: Core PVE services still active
|
|
shell: |
|
|
for s in pve-cluster corosync pvedaemon pveproxy pvestatd; do
|
|
a=$(systemctl is-active $s 2>&1); printf " %-14s %s\n" "$s" "$a"
|
|
test "$a" = "active" || bad=1
|
|
done
|
|
test -z "$bad"
|
|
changed_when: "false"
|
|
|
|
- name: Cluster still quorate after the upgrade
|
|
shell: pvecm status 2>/dev/null | grep -E "Quorate|Total votes"
|
|
changed_when: "false"
|
|
|
|
- name: Surface any conffiles the confold policy left unapplied
|
|
shell: |
|
|
found=$(find /etc -name "*.dpkg-dist" -o -name "*.dpkg-new" 2>/dev/null | head -20)
|
|
if [ -n "$found" ]; then
|
|
echo "REVIEW THESE — upstream shipped changes that were NOT applied:"; echo "$found"
|
|
else
|
|
echo "no unapplied conffiles"
|
|
fi
|
|
changed_when: "false"
|
|
|
|
- name: Report whether a reboot is required
|
|
shell: |
|
|
run=$(uname -r)
|
|
new=$(ls -1 /boot/vmlinuz-* 2>/dev/null | sed 's|.*/vmlinuz-||' | sort -V | tail -1)
|
|
echo " running kernel: $run"
|
|
echo " newest on disk: $new"
|
|
[ "$run" != "$new" ] && echo " -> REBOOT REQUIRED to run $new" || echo " -> no kernel change"
|
|
changed_when: "false"
|