diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8be2044..b45f3a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,7 +105,7 @@ jobs: - name: pytest run: pytest -q controlplane/tests - name: shellcheck - run: sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck && shellcheck --severity=warning $(git ls-files '*.sh') debian/postinst debian/prerm debian/postrm + run: sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck && shellcheck --severity=warning $(git ls-files '*.sh') debian/preinst debian/postinst debian/prerm debian/postrm - name: reuse (license compliance) run: reuse lint - name: blocklist smoke @@ -540,7 +540,7 @@ jobs: # --reinstall --allow-downgrades: on main the build is regularly version-equal # to the release, and a plain install would say "already the newest version" # and test nothing. dpkg must run the postinst with $2 set, or its upgrade - # branch (try-restart, update-all, reconcile) never runs. + # branch (restart, update-all, reconcile) never runs. sudo apt-get install -y --reinstall --allow-downgrades ./packages/*.deb 2>&1 \ | tee upgrade.log @@ -645,6 +645,44 @@ jobs: echo '```' } >> "$GITHUB_STEP_SUMMARY" + # Debian's rule from 7.3.7 on: an upgrade keeps a service the admin switched off. Once + # more from the published release, which never recorded deb-systemd-helper state (so this + # is the preinst's one-time transition), then a second time on top: the recorded state + # must hold. + # The disabled service is neither started for a moment nor used for the instance update, and + # apt shows the one line that says so. + - name: A service disabled before the upgrade stays disabled and stopped + if: steps.prev.outputs.skip != '1' + run: | + set -eux + set -o pipefail + P=linuxmuster-squid + U=$P.service + STATE=/var/lib/systemd/deb-systemd-helper-enabled/$U.dsh-also + sudo apt-get purge -y linuxmuster-squid + test ! -e "$STATE" # purge forgot the state: the release starts from scratch + sudo apt-get install -y ./previous/*.deb + test "$(systemctl is-enabled $U)" = enabled + sudo systemctl disable --now $U + test "$(systemctl is-active $U || true)" = inactive + for pass in 1 2; do + cursor="$(sudo journalctl -q -n 0 --show-cursor | sed -n 's/^-- cursor: //p')" + test -n "$cursor" + sudo apt-get install -y --reinstall --allow-downgrades ./packages/*.deb 2>&1 \ + | tee "disabled-$pass.log" + grep -E 'Unpacking linuxmuster-squid \([^)]+\) over \([^)]+\)' "disabled-$pass.log" + test "$(systemctl is-enabled $U || true)" = disabled + test "$(systemctl is-active $U || true)" = inactive + test "$(sudo journalctl -q --no-pager --after-cursor="$cursor" -u $U \ + | grep -c -F 'Started ' || true)" = 0 + grep -F "$P: the service is disabled and not running, instances not updated" \ + "disabled-$pass.log" + done + test -e "$STATE" # the transition recorded the admin's decision + test -z "$(dpkg --audit)" + echo "### disabled before the upgrade: disabled and stopped after two upgrades" \ + >> "$GITHUB_STEP_SUMMARY" + - name: Journal of the control plane (always, for diagnosis) if: always() && steps.prev.outputs.skip != '1' run: sudo journalctl -u linuxmuster-squid.service --no-pager | tail -60 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd03df2..295dd10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -451,7 +451,7 @@ jobs: # --reinstall --allow-downgrades: on main the build is regularly version-equal # to the release, and a plain install would say "already the newest version" # and test nothing. dpkg must run the postinst with $2 set, or its upgrade - # branch (try-restart, update-all, reconcile) never runs. + # branch (restart, update-all, reconcile) never runs. sudo apt-get install -y --reinstall --allow-downgrades ./packages/*.deb 2>&1 \ | tee upgrade.log @@ -556,6 +556,44 @@ jobs: echo '```' } >> "$GITHUB_STEP_SUMMARY" + # Debian's rule from 7.3.7 on: an upgrade keeps a service the admin switched off. Once + # more from the published release, which never recorded deb-systemd-helper state (so this + # is the preinst's one-time transition), then a second time on top: the recorded state + # must hold. + # The disabled service is neither started for a moment nor used for the instance update, and + # apt shows the one line that says so. + - name: A service disabled before the upgrade stays disabled and stopped + if: steps.prev.outputs.skip != '1' + run: | + set -eux + set -o pipefail + P=linuxmuster-squid + U=$P.service + STATE=/var/lib/systemd/deb-systemd-helper-enabled/$U.dsh-also + sudo apt-get purge -y linuxmuster-squid + test ! -e "$STATE" # purge forgot the state: the release starts from scratch + sudo apt-get install -y ./previous/*.deb + test "$(systemctl is-enabled $U)" = enabled + sudo systemctl disable --now $U + test "$(systemctl is-active $U || true)" = inactive + for pass in 1 2; do + cursor="$(sudo journalctl -q -n 0 --show-cursor | sed -n 's/^-- cursor: //p')" + test -n "$cursor" + sudo apt-get install -y --reinstall --allow-downgrades ./packages/*.deb 2>&1 \ + | tee "disabled-$pass.log" + grep -E 'Unpacking linuxmuster-squid \([^)]+\) over \([^)]+\)' "disabled-$pass.log" + test "$(systemctl is-enabled $U || true)" = disabled + test "$(systemctl is-active $U || true)" = inactive + test "$(sudo journalctl -q --no-pager --after-cursor="$cursor" -u $U \ + | grep -c -F 'Started ' || true)" = 0 + grep -F "$P: the service is disabled and not running, instances not updated" \ + "disabled-$pass.log" + done + test -e "$STATE" # the transition recorded the admin's decision + test -z "$(dpkg --audit)" + echo "### disabled before the upgrade: disabled and stopped after two upgrades" \ + >> "$GITHUB_STEP_SUMMARY" + - name: Journal of the control plane (always, for diagnosis) if: always() && steps.prev.outputs.skip != '1' run: sudo journalctl -u linuxmuster-squid.service --no-pager | tail -60 diff --git a/RELEASE.md b/RELEASE.md index 75f6ba6..1e143cb 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -52,7 +52,8 @@ does not yet exist — every `lmnsquid create --image ghcr.io/…@sha256: Mon, 28 Sep 2026 15:29:04 +0200 + linuxmuster-squid (7.3.6) lmn73; urgency=medium * A git step of the instance change log that fails during the package diff --git a/debian/postinst b/debian/postinst index a682ca4..08077ce 100644 --- a/debian/postinst +++ b/debian/postinst @@ -113,21 +113,47 @@ fi # blanket 0755), so anything intentionally restrictive stays as restrictive as it was. chmod -R go-w /opt/linuxmuster-squid +# The service by Debian's rules, as dh_installsystemd's snippets do it, by hand because the +# unit stays in /lib/systemd/system (moving it to /usr/lib within one package risks losing it +# under usrmerge). deb-systemd-helper keeps the admin's decision: a new installation enables +# the service, afterwards it stays as the admin left it, so `systemctl disable` survives +# upgrades (up to 7.3.6 every configure enabled it again). The preinst recorded the state of a +# server coming from 7.3.6 or older. Without a running systemd (image build, chroot) only the +# links are made and the service starts with the next boot. +UNIT=linuxmuster-squid.service +# unmask as dh_installsystemd still does: it lifts only a mask deb-systemd-helper set itself +# (older debhelper masked on remove), never one of the admin. +deb-systemd-helper unmask "$UNIT" >/dev/null || true +if deb-systemd-helper --quiet was-enabled "$UNIT"; then + deb-systemd-helper enable "$UNIT" >/dev/null || true +else + deb-systemd-helper update-state "$UNIT" >/dev/null || true +fi + if [ -d /run/systemd/system ]; then systemctl daemon-reload || true - systemctl enable linuxmuster-squid.service || true - # Load new code on a genuine upgrade (try-restart no-ops if the service is not - # running), then start unconditionally so it is up in ALL cases -- fresh install - # AND reinstall over a removed/config-files state, where try-restart alone would - # leave it stopped. `start` is a no-op if it is already running. - if [ -n "${2:-}" ]; then - systemctl try-restart linuxmuster-squid.service || true + # Enabled: restart on an upgrade (loads the new code, starts a stopped service), start on + # a new installation. Disabled or masked: only try-restart, so a service that still runs + # gets the new code and a stopped one stays stopped (restart would do the same for a + # disabled unit but print "not starting it", and it fails on a masked unit that runs). + # deb-systemd-invoke honours policy-rc.d. + if [ "$(systemctl is-enabled "$UNIT" 2>/dev/null)" = enabled ]; then + if [ -n "${2:-}" ]; then + deb-systemd-invoke restart "$UNIT" || true + else + deb-systemd-invoke start "$UNIT" || true + fi + else + deb-systemd-invoke try-restart "$UNIT" || true fi - systemctl start linuxmuster-squid.service || true - # On upgrade, lift all running instances onto this package's pinned default - # image (best-effort; each instance updates with its own health-check - # auto-rollback). Wait briefly for the restarted API; never fail the install. - if [ -n "${2:-}" ]; then + # On upgrade, lift all instances onto this package's pinned default image (best-effort; + # each instance updates with its own health-check auto-rollback), but only through a + # service that runs after this configure; a service the admin keeps off is not started + # for it. Wait briefly for the restarted API; never fail the install. + if [ -n "${2:-}" ] && ! systemctl is-active --quiet "$UNIT"; then + echo "linuxmuster-squid: the service is $(systemctl is-enabled "$UNIT" 2>/dev/null)" \ + "and not running, instances not updated; run 'lmnsquid update-all' after starting it" + elif [ -n "${2:-}" ]; then LMN=/opt/linuxmuster-squid/venv/bin/lmnsquid i=0 while [ "$i" -lt 30 ]; do diff --git a/debian/postrm b/debian/postrm index 88d3bd8..dfef95b 100644 --- a/debian/postrm +++ b/debian/postrm @@ -7,6 +7,11 @@ if [ -d /run/systemd/system ]; then fi # Deliberately do NOT delete config/state + venv on remove (only on purge). if [ "$1" = "purge" ]; then + # The admin's enable/disable decision survives a remove (the prerm only stops the service) + # and is forgotten here, as debhelper 13.14's dh_installsystemd snippet does it (no mask on + # remove, no unmask on purge since debhelper 13.10). deb-systemd-helper comes with + # init-system-helpers, which is Essential. + deb-systemd-helper purge linuxmuster-squid.service >/dev/null || true rm -rf /opt/linuxmuster-squid rm -rf /etc/linuxmuster-squid /var/lib/linuxmuster-squid # The safe.directory entry the postinst added for the instances repo. diff --git a/debian/preinst b/debian/preinst new file mode 100644 index 0000000..d976e39 --- /dev/null +++ b/debian/preinst @@ -0,0 +1,27 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Kevin Stenzel +# SPDX-License-Identifier: GPL-3.0-or-later +set -e + +# One-time transition to Debian's service rules (7.3.7). Up to 7.3.6 the maintainer scripts +# enabled the service by hand and never recorded deb-systemd-helper state, and without that +# state `deb-systemd-helper was-enabled` answers "enabled" even for a service the admin +# disabled, so the postinst would enable it again. On an upgrade of the installed package the +# old unit and the admin's choice are still in place: record them now, before anything +# changes. Enabled -> enable (records the existing link), disabled or masked -> update-state +# (known, not enabled). Not while systemd is down (an image build, a chroot): the old postinst +# enabled the service only while systemd ran, so a missing link there is no admin decision, +# and the postinst enables it like a new installation. A reinstall from the config-files state +# ("install ") is left alone for the same reason: the old prerm disabled the service +# on remove, the admin did not. +UNIT=linuxmuster-squid.service +if [ "$1" = upgrade ] && ! deb-systemd-helper debian-installed "$UNIT"; then + if [ "$(systemctl is-enabled "$UNIT" 2>/dev/null)" = enabled ]; then + deb-systemd-helper enable "$UNIT" >/dev/null || true + elif [ -d /run/systemd/system ]; then + deb-systemd-helper update-state "$UNIT" >/dev/null || true + fi +fi + +#DEBHELPER# +exit 0 diff --git a/debian/prerm b/debian/prerm index 11ba201..2f92977 100644 --- a/debian/prerm +++ b/debian/prerm @@ -2,11 +2,13 @@ # SPDX-FileCopyrightText: Kevin Stenzel # SPDX-License-Identifier: GPL-3.0-or-later set -e -# Stop/disable ONLY on a real remove. On upgrade ($1=upgrade), keep the service -# RUNNING so postinst can restart it via try-restart (new code); stopping it here -# would leave it inactive after the upgrade. +# Stop ONLY on a real remove. On upgrade ($1=upgrade), keep the service RUNNING so the +# postinst restarts it (new code); stopping it here would leave it inactive after the +# upgrade. Whether it is enabled stays the admin's decision (Debian's rule, as +# dh_installsystemd does it): a reinstall after a remove restores it, a purge forgets it +# (postrm). Up to 7.3.6 remove also disabled it. if [ "$1" = remove ] && [ -d /run/systemd/system ]; then - systemctl disable --now linuxmuster-squid.service || true + deb-systemd-invoke stop linuxmuster-squid.service >/dev/null || true fi #DEBHELPER# exit 0 diff --git a/debian/rules b/debian/rules index d6a3259..fff552f 100755 --- a/debian/rules +++ b/debian/rules @@ -40,7 +40,8 @@ override_dh_auto_clean: override_dh_strip override_dh_dwz override_dh_strip_nondeterminism override_dh_makeshlibs: @: -# The maintainer scripts handle the units by hand, unchanged since 7.3.x (the plan, §4). +# The maintainer scripts handle the unit by hand, with deb-systemd-helper and deb-systemd-invoke +# as dh_installsystemd's snippets do, because the unit stays in /lib/systemd/system. override_dh_installsystemd: @: diff --git a/docs/install.md b/docs/install.md index 4377da8..cb335c9 100644 --- a/docs/install.md +++ b/docs/install.md @@ -46,8 +46,8 @@ lmnsquid version # {"version": "7.3.6"} (= dpkg-query -W linuxmuster-sq The postinst creates the system user `lmnsquid` (in group `docker`), the config `/etc/linuxmuster-squid/config.yml` with a random API token (0600), `secrets/` (0700) and `blocklists/`, the git repository `/var/lib/linuxmuster-squid/instances` (the change log of -the instance definitions), and starts the service on `127.0.0.1:8080`. The admin scripts used -below are installed under `/usr/share/linuxmuster-squid/scripts/`. +the instance definitions), and enables and starts the service on `127.0.0.1:8080`. The admin +scripts used below are installed under `/usr/share/linuxmuster-squid/scripts/`. ## 3. Service account and keytab (on the DC) @@ -164,10 +164,14 @@ Assign the proxy per role by GPO (silent Kerberos SSO) and force the proxy at th ## 9. Updates ```bash -apt-get install -y ./linuxmuster-squid__amd64.deb # restarts the service, then update-all +apt-get install -y ./linuxmuster-squid__amd64.deb # restarts the service, then update-all (*) lmnsquid update-all # on demand: every instance -> pinned default image ``` +(*) Only while the service is enabled or still running: a service switched off with +`systemctl disable --now` stays off, and the instances are not updated +([operations.md](operations.md#keeping-the-service-off)). + Every instance update is health-gated with automatic rollback: the replacement container is created first, the previous one is kept until the new one is **healthy** and restarted if it is not. **Upgrading from 7.3.0:** containers created with 7.3.0 run without the blocklist diff --git a/docs/operations.md b/docs/operations.md index 515705e..49a2cfb 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -16,8 +16,9 @@ apt install ./linuxmuster-squid__amd64.deb # or from the lmn73 apt re systemctl status linuxmuster-squid # should be "active" ``` The postinst creates the system user `lmnsquid` (in group `docker`), generates a -random API token in `/etc/linuxmuster-squid/config.yml` (0600) and starts the service -(bound to `127.0.0.1:8080`). +random API token in `/etc/linuxmuster-squid/config.yml` (0600) and enables and starts the +service (bound to `127.0.0.1:8080`). After that the package keeps the admin's decision: +[Keeping the service off](#keeping-the-service-off). ## Managing instances (CLI = thin client of the REST API) @@ -105,10 +106,12 @@ The update pulls the new digest, replaces the container, waits for `healthy` and belongs in production is decided by a **merged PR** (never auto-merge; raised by hand while Renovate is disabled). -On a **`.deb` upgrade** the postinst runs `update-all` automatically (best-effort): all -instances are lifted onto that package's pinned default image, each with its own health-check -auto-rollback; instances already on the default are skipped, and the apt transaction never -fails over this. Run `lmnsquid update-all` yourself any time to do the same on demand. +On a **`.deb` upgrade** the postinst runs `update-all` automatically (best-effort) when the +service runs after the upgrade: all instances are lifted onto that package's pinned default +image, each with its own health-check auto-rollback; instances already on the default are +skipped, and the apt transaction never fails over this. A service the admin switched off is not +started for it ([Keeping the service off](#keeping-the-service-off)). Run `lmnsquid update-all` +yourself any time to do the same on demand. **Upgrade from 7.3.0 (or older) to 7.3.1:** instances created before 7.3.1 have no blocklist mount, and `update-all` skips them when the default image did not move. The postinst therefore @@ -130,6 +133,39 @@ instances are still reconciled). A container that already matches its definition `lmnsquid.spec`) is left running — so `lmnsquid reconcile` is safe to run at any time and an `edit` that changes nothing causes no restart. Use `lmnsquid restart ` for a plain restart. +## Keeping the service off + +From 7.3.7 on the package handles its service by Debian's rules (`deb-systemd-helper` and +`deb-systemd-invoke`, as packages built with dh_installsystemd do): a new installation enables +and starts `linuxmuster-squid`; after that, upgrades keep what the admin decided. + +- **Switch it off and keep it off:** `systemctl disable --now linuxmuster-squid`. Upgrades, + reinstalls and `dpkg-reconfigure` leave it disabled and stopped, do not update the instances + and print one line in the apt output: `linuxmuster-squid: the service is disabled and not + running, instances not updated; run 'lmnsquid update-all' after starting it`. + `systemctl stop` alone is not enough: the next upgrade starts an enabled service again. +- **The proxies are separate:** the Squid containers are Docker containers + (`unless-stopped`) and keep running without the control plane. If they must be off too, stop + them first with `lmnsquid stop ` (it needs the running service). +- **Switch it on again:** `systemctl enable --now linuxmuster-squid`, then `lmnsquid update-all` + for the instance updates the upgrades skipped. +- **Disabled but still running** (`systemctl disable` without `--now`): an upgrade restarts it + with the new code and updates the instances; it stays disabled. +- **Masked** (`systemctl mask`): the package leaves it alone. +- **Remove and reinstall:** `apt remove` only stops the service and keeps the decision; a + reinstall brings it back as it was. `apt purge` forgets it: the next installation is a new one, + enabled and started. A reinstall after removing 7.3.6 or older is a new installation as well + (those versions disabled the service on remove). +- **The first upgrade to 7.3.7** takes over the state the service is in at that moment, once: + 7.3.6 and older kept no record of it. Enabled stays enabled, disabled or masked stays so. + One exception: if this first upgrade runs without a running systemd (an image build, a + chroot), a service without an enable link is treated like a new installation and enabled, + because 7.3.6 and older never enabled it offline. Upgrades with systemd running, and every + later upgrade, keep a service the admin disabled off. +- **Downgrade to 7.3.6 or older:** that version enables and starts the service again. +- **Without a running systemd** (an image build, a chroot) a new installation only enables the + service; it starts with the next boot. + ## Observing ``` @@ -233,7 +269,8 @@ lmnsquid reconcile # reads the desired state + pulls the pinned digests -> healthy); one failing instance does not stop the rest (`failed` list, CLI exit 1). - **Reboot** doesn't need this: `restart_policy: unless-stopped` brings running containers back. - **Downgrade the tool:** install an older `.deb` → the postinst restarts the service - (loads the old code). **Cache volume broken** (container stays unhealthy after a power outage): + (loads the old code); 7.3.6 and older also enable it again. **Cache volume broken** + (container stays unhealthy after a power outage): `docker rm -f ` + `docker volume rm lmnsquid-cache-` → `lmnsquid reconcile`. ## Security posture (brief) diff --git a/docs/test-strategy.md b/docs/test-strategy.md index 83ef363..cf9938c 100644 --- a/docs/test-strategy.md +++ b/docs/test-strategy.md @@ -115,7 +115,9 @@ acceptance list in `deployment-gpo.md`. failed step and the repository, the package stays `ii` and the service as enabled/active as before, and once `HEAD` is back the next configure commits again without a warning; upgrade over a release with root-owned files in the change log repository → handed - back to `lmnsquid`, history kept. + back to `lmnsquid`, history kept; a service the admin disabled (`systemctl disable + --now`) before the upgrade from the release stays disabled and stopped across two upgrades + in a row, with the "instances not updated" line in the apt output (CI upgrade-smoke). - **P10:** keytab perms; manager ACL not reachable externally; API bind ≠ 0.0.0.0; bypass/traversal; DC outage does not stall (ttl/grace).