diff --git a/README.md b/README.md index 53f7159..31a9010 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,121 @@ # apt-pacman-hook -Ubuntu apt hook for automatic snapshots \ No newline at end of file +Ubuntu APT hook for automatic ZFS snapshots. + +This is the Ubuntu/APT counterpart to [zfs-pacman-hook](https://quico.space/quico-os-setup/zfs-pacman-hook). It snapshots selected ZFS datasets before APT invokes `dpkg`, includes affected Debian package names in snapshot names, and prunes old snapshots using separate ordinary and important-package retention chains. + +## Setup + +Install the files as follows: + +```bash +sudo install -Dm755 apt-zfs-snapshot.sh /usr/local/sbin/apt-zfs-snapshot +sudo install -Dm644 apt-zfs-snapshot.conf /etc/apt-zfs-snapshot.conf +sudo install -Dm644 90apt-zfs-snapshot.conf /etc/apt/apt.conf.d/90apt-zfs-snapshot.conf +``` + +Set `dry_run='true'` in `/etc/apt-zfs-snapshot.conf`, run a harmless APT operation, and inspect the planned names. Set it back to `false` after verifying dataset selection and naming. + +The helper runs as root through APT and requires Bash, `zfs`, and `findmnt`. + +## APT integration + +The hook uses `DPkg::Pre-Install-Pkgs`. APT sends its version-3 package action protocol on the file descriptor named by `APT_HOOK_INFO_FD`. Records contain the package name, old and new versions, version direction, architecture, and action. This captures dependencies selected by APT as well as explicitly requested packages. + +Actions are classified as follows: + +| APT record | Operation marker | +| --- | --- | +| No old version and a new version | `inst` | +| New version greater than old version | `upgr` | +| New version lower than old version | `down` | +| `**REMOVE**` action | `rmvl` | + +Configuration-only records are ignored. If one callback contains multiple operation classes, the marker is `mixd`. In normal Ubuntu use, an `apt-get -y dist-upgrade` produces one useful callback and therefore one snapshot, matching the existing `DPkg::Pre-Invoke` stop-gap. APT can invoke `dpkg` more than once for complex operations; this implementation creates one snapshot per package-action callback. + +## Dataset selection + +By default, datasets with this exact ZFS property are selected: + +```bash +zfs set space.quico:auto-snapshot=true rpool/ROOT/ubuntu +``` + +With `snap_only_local_datasets='true'`, only mounted ZFS datasets are selected. This avoids snapshotting another operating system stored in the same pool. To explicitly select roots, set: + +```bash +snapshot_roots='bpool/BOOT rpool/ROOT' +``` + +Explicit roots bypass property discovery and the mounted-dataset check. Each root is snapshotted recursively, and all roots are passed to one `zfs snapshot -r` command. + +## Snapshot names + +Names use this form: + +```text +dataset@apt_DATE_COUNTER_op:OP_sev:SEVERITY_pkgs:PACKAGE-LIST +``` + +Example: + +```text +rpool/ROOT/ubuntu@apt_2026-09-16-1430_1_op:upgr_sev:imp_pkgs:linux-image-6.17:systemd:libc6 +``` + +The timestamp is UTC by default and is controlled by `date_format` and `timezone`. The counter starts at one and increases if a complete name already exists. This prevents collisions when identical operations happen within the same timestamp interval. + +Package names are separated with `package_separator`, which defaults to `:`. Plus signs are replaced with underscores before inclusion in a ZFS name. The package list is shortened by dropping complete names from the end first, then truncating the final remaining name and adding `...` when at least four characters fit. `packages_max_length` is a best effort because the complete ZFS name is limited to 255 characters. If the fixed dataset and metadata fields cannot fit, the hook refuses to create a snapshot. + +## Retention + +If any affected package matches `important_names`, the snapshot belongs to the important chain. Otherwise it belongs to the trivial chain. The default important expression is: + +```text +linux-image(-.*)?|linux-headers(-.*)?|systemd|zfs-(dkms|utils) +``` + +The defaults retain 25 trivial snapshots and 10 important snapshots. Retention runs only after a successful snapshot and only considers snapshots with this tool's prefix and the current severity marker. Recursive snapshot trees are destroyed with `zfs destroy -r`. + +## Configuration + +`/etc/apt-zfs-snapshot.conf` is sourced as a Bash fragment. Available parameters are: + +| Parameter | Default | Meaning | +| --- | --- | --- | +| `dry_run` | `false` | Print planned snapshots without changing ZFS | +| `important_names` | kernel/systemd/ZFS expression | Exact regex for important packages | +| `snapshots_trivial_keep` | `25` | Trivial snapshots retained per dataset | +| `snapshots_important_keep` | `10` | Important snapshots retained per dataset | +| `trivial_suffix` | `trv` | Ordinary severity marker | +| `important_suffix` | `imp` | Important severity marker | +| `packages_max_length` | `30` | Maximum package-list characters | +| `snap_only_local_datasets` | `true` | Restrict property-selected datasets to mounted datasets | +| `snapshot_roots` | empty | Explicit space-separated recursive snapshot roots | +| `field_separator` | `_` | Separator between snapshot fields | +| `package_separator` | `:` | Separator between package names | +| `snapshot_prefix` | `apt` | Prefix identifying these snapshots | +| `date_format` | `%F-%H%M` | Format passed to `date` | +| `timezone` | `Etc/UTC` | Timestamp timezone | +| `install_suffix` | `inst` | Install marker | +| `remove_suffix` | `rmvl` | Remove marker | +| `upgrade_suffix` | `upgr` | Upgrade marker | +| `downgrade_suffix` | `down` | Downgrade marker | +| `mixed_suffix` | `mixd` | Multiple operation classes marker | + +The property name and value are fixed to `space.quico:auto-snapshot=true`, matching the original tool. + +## Existing stop-gap hook + +The previous `DPkg::Pre-Invoke` hook has no package-list input. This project uses `DPkg::Pre-Install-Pkgs` to retain the same practical pre-`dpkg` placement while adding package-aware names. Do not run both hooks unless two snapshots per APT operation are intentional. + +## Limitations + +- The hook covers package changes when APT invokes `dpkg`; direct `dpkg` operations are not intercepted. +- APT may invoke `dpkg` more than once in complex cases, producing more than one snapshot. +- The snapshot occurs before the corresponding `dpkg` invocation, not before APT downloads packages or resolves dependencies. +- Snapshot failures are reported but do not abort the package operation, matching the warning-oriented original behavior. + +## License + +MIT. See [LICENSE](LICENSE).