Date: Thu, 14 May 2026 13:59:59 +0200 (CEST) From: Ronald Klop <ronald-lists@klop.ws> To: freebsd-pkg@freebsd.org Subject: Re: pkg-be-plugin: auto-create ZFS boot environments before pkg transactions Message-ID: <1733843605.178.1778759999712@localhost> In-Reply-To: <cace1267-3dd1-463c-aacb-5d57b89e9cf1@starnix.net>
index | next in thread | previous in thread | raw e-mail
[-- Attachment #1 --] Looks useful! Are you planning to create a port for this? Regards, Ronald. Van: Sasha Karcz <sasha@starnix.net> Datum: donderdag, 14 mei 2026 07:30 Aan: freebsd-pkg@freebsd.org Onderwerp: pkg-be-plugin: auto-create ZFS boot environments before pkg transactions > > Hello, > > I've written a pkg(8) plugin that automatically creates a ZFS boot environment before each install, upgrade, and deinstall transaction. If a transaction leaves the system in a broken state, the pre-transaction BE is there to boot into. > > The plugin is called pkg-be-plugin and installs as be.so. It uses libbe(3) directly — no exec of bectl(8) or zfs(8). > > Behaviour > > On each covered transaction, the plugin calls libbe_init() and be_create() to snapshot the current BE under a timestamped name (default prefix: pre-pkg, e.g. pre-pkg-20260514-091532). After creation, it prunes older auto-created BEs to keep the count at or below a configurable limit, with a minimum-age guard so recent rollback points aren't destroyed even when over the limit. > > All activity is logged to syslog(3) at LOG_NOTICE for normal operations and LOG_WARNING/LOG_ERR for failures, so admins can grep /var/log/messages to find BE names for rollback after a bad transaction. > > Configuration (via /usr/local/etc/pkg/be.conf, UCL format) > > BE_PLUGIN_ENABLED — master switch (default: true) > BE_PLUGIN_KEEP — maximum BEs to retain (default: 5) > BE_PLUGIN_NAME_PREFIX — name prefix (default: pre-pkg) > BE_PLUGIN_MIN_AGE — minimum age before pruning (default: 7d; protects recent rollback points from being destroyed when count exceeds KEEP) > BE_PLUGIN_STRICT — abort transaction on BE creation failure (default: false) > BE_PLUGIN_SKIP_TRANSACTIONS — comma-separated list of transaction types to skip (install, upgrade, deinstall) > Non-ZFS systems > > libbe_init() fails on UFS roots and in jails without ZFS access. In non-strict mode (the default) this is logged as a warning and the transaction proceeds normally. Strict mode causes a fail-closed abort, which may be appropriate for ZFS-only fleets. > > Testing > > Tested on FreeBSD 15.0-RELEASE-p5 with the install/upgrade/deinstall transaction types, including multi-package transactions, the prune path (over-KEEP and under-min-age scenarios), and strict-mode behaviour. Unit tests cover the config parser and prune sort/filter logic. > > Source > > https://github.com/usenix17/pkg-be-plugin > > Feedback welcome. Specific things I'd appreciate eyes on: > > pkg plugin API usage — particularly the hook lifecycle (init multiple hooks shutdown) and whether PKG_PLUGIN_HOOK_PRE_{INSTALL,UPGRADE,DEINSTALL} are the right hooks for this purpose, or whether there's a less-surprising place to do BE creation. > libbe nvlist property access — the creation property is stored as a string of decimal Unix epoch seconds rather than a uint64. I worked this out via integration testing; if this is documented somewhere I missed, pointers welcome. > Prune semantics — currently the count can drift above KEEP if all candidate BEs are under MIN_AGE. Trade-off chosen for the homelab-friendly "never destroy a recent rollback" property. If list consensus prefers strict-count enforcement, the policy is a one-line change. > Sasha Karcz > > > [-- Attachment #2 --] <html><head></head><body>Looks useful!<br> <br> Are you planning to create a port for this?<br> <br> Regards,<br> Ronald.<br> <br> <p><strong>Van:</strong> Sasha Karcz <sasha@starnix.net><br> <strong>Datum:</strong> donderdag, 14 mei 2026 07:30<br> <strong>Aan:</strong> freebsd-pkg@freebsd.org<br> <strong>Onderwerp:</strong> pkg-be-plugin: auto-create ZFS boot environments before pkg transactions</p> <blockquote style="padding-right: 0px; padding-left: 5px; margin-left: 5px; border-left: #000000 2px solid; margin-right: 0px"> <div class="MessageRFC822Viewer" id="P"> <div class="MultipartMixedViewer"> <div class="MultipartMixedViewer"> <div class="MultipartMixedViewer"> <div class="MultipartAlternativeViewer"> <div class="TextHTMLViewer" id="P.P.P1.P1.P1.P"> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]">Hello,</p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]">I've written a pkg(8) plugin that automatically creates a ZFS boot environment before each install, upgrade, and deinstall transaction. If a transaction leaves the system in a broken state, the pre-transaction BE is there to boot into.</p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]">The plugin is called pkg-be-plugin and installs as be.so. It uses libbe(3) directly — no exec of bectl(8) or zfs(8).</p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"><strong>Behaviour</strong></p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]">On each covered transaction, the plugin calls <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">libbe_init()</code> and <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">be_create()</code> to snapshot the current BE under a timestamped name (default prefix: <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">pre-pkg</code>, e.g. <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">pre-pkg-20260514-091532</code>). After creation, it prunes older auto-created BEs to keep the count at or below a configurable limit, with a minimum-age guard so recent rollback points aren't destroyed even when over the limit.</p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]">All activity is logged to syslog(3) at LOG_NOTICE for normal operations and LOG_WARNING/LOG_ERR for failures, so admins can grep <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">/var/log/messages</code> to find BE names for rollback after a bad transaction.</p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"><strong>Configuration</strong> (via <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">/usr/local/etc/pkg/be.conf</code>, UCL format)</p> <ul class="[li_&]:mb-0 [li_&]:mt-1 [li_&]:gap-1 [&:not(:last-child)_ul]:pb-1 [&:not(:last-child)_ol]:pb-1 list-disc flex flex-col gap-1 pl-8 mb-3"> <li class="font-claude-response-body whitespace-normal break-words pl-2"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">BE_PLUGIN_ENABLED</code> — master switch (default: true)</li> <li class="font-claude-response-body whitespace-normal break-words pl-2"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">BE_PLUGIN_KEEP</code> — maximum BEs to retain (default: 5)</li> <li class="font-claude-response-body whitespace-normal break-words pl-2"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">BE_PLUGIN_NAME_PREFIX</code> — name prefix (default: <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">pre-pkg</code>)</li> <li class="font-claude-response-body whitespace-normal break-words pl-2"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">BE_PLUGIN_MIN_AGE</code> — minimum age before pruning (default: 7d; protects recent rollback points from being destroyed when count exceeds KEEP)</li> <li class="font-claude-response-body whitespace-normal break-words pl-2"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">BE_PLUGIN_STRICT</code> — abort transaction on BE creation failure (default: false)</li> <li class="font-claude-response-body whitespace-normal break-words pl-2"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">BE_PLUGIN_SKIP_TRANSACTIONS</code> — comma-separated list of transaction types to skip (<code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">install</code>, <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">upgrade</code>, <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">deinstall</code>)</li> </ul> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"><strong>Non-ZFS systems</strong></p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"><code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">libbe_init()</code> fails on UFS roots and in jails without ZFS access. In non-strict mode (the default) this is logged as a warning and the transaction proceeds normally. Strict mode causes a fail-closed abort, which may be appropriate for ZFS-only fleets.</p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"><strong>Testing</strong></p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]">Tested on FreeBSD 15.0-RELEASE-p5 with the install/upgrade/deinstall transaction types, including multi-package transactions, the prune path (over-KEEP and under-min-age scenarios), and strict-mode behaviour. Unit tests cover the config parser and prune sort/filter logic.</p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"><strong>Source</strong></p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]"><a class="underline underline-offset-2 decoration-1 decoration-current/40 hover:decoration-current focus:decoration-current moz-txt-link-freetext" href="https://github.com/usenix17/pkg-be-plugin">https://github.com/usenix17/pkg-be-plugin</a></p> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]">Feedback welcome. Specific things I'd appreciate eyes on:</p> <ol class="[li_&]:mb-0 [li_&]:mt-1 [li_&]:gap-1 [&:not(:last-child)_ul]:pb-1 [&:not(:last-child)_ol]:pb-1 list-decimal flex flex-col gap-1 pl-8 mb-3"> <li class="font-claude-response-body whitespace-normal break-words pl-2"><strong>pkg plugin API usage</strong> — particularly the hook lifecycle (init multiple hooks shutdown) and whether <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">PKG_PLUGIN_HOOK_PRE_{INSTALL,UPGRADE,DEINSTALL}</code> are the right hooks for this purpose, or whether there's a less-surprising place to do BE creation.</li> <li class="font-claude-response-body whitespace-normal break-words pl-2"><strong>libbe nvlist property access</strong> — the <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">creation</code> property is stored as a string of decimal Unix epoch seconds rather than a uint64. I worked this out via integration testing; if this is documented somewhere I missed, pointers welcome.</li> <li class="font-claude-response-body whitespace-normal break-words pl-2"><strong>Prune semantics</strong> — currently the count can drift above <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">KEEP</code> if all candidate BEs are under <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">MIN_AGE</code>. Trade-off chosen for the homelab-friendly "never destroy a recent rollback" property. If list consensus prefers strict-count enforcement, the policy is a one-line change.</li> </ol> <p class="font-claude-response-body break-words whitespace-normal leading-[1.7]">Sasha Karcz</p> </div> </div> <div class="DefaultViewer"> </div> </div> </div> <div class="DefaultViewer"> </div> </div> </div> </blockquote> <br> </body></html>home | help
Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?1733843605.178.1778759999712>
