Skip site navigation (1)Skip section navigation (2)
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>
&nbsp;
<p><strong>Van:</strong> Sasha Karcz &lt;sasha@starnix.net&gt;<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_&amp;]:mb-0 [li_&amp;]:mt-1 [li_&amp;]:gap-1 [&amp;:not(:last-child)_ul]:pb-1 [&amp;: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_&amp;]:mb-0 [li_&amp;]:mt-1 [li_&amp;]:gap-1 [&amp;:not(:last-child)_ul]:pb-1 [&amp;: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">&nbsp;</div>
</div>
</div>

<div class="DefaultViewer">&nbsp;</div>
</div>
</div>
</blockquote>
<br>
&nbsp;</body></html>
home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?1733843605.178.1778759999712>