FreeBSD jail estate inventory: jails, ZFS datasets, snapshots and listening sockets, and the disagreements between them
| Tier | operations |
| Role | infrastructure adapter |
| Path | crates/operations/jail-inventory |
| Edition | 2021 |
| Targets | estate-report, operations_jail_inventory, estate |
| Public items | 37 across 5 modules |
| Tests | 24 |
What it is for
FreeBSD jail estate inventory: what a host actually holds, and where its own records disagree with its disks.
operations_host_inventory answers what the hardware is. This crate answers what the software estate is: the jails, the ZFS datasets and snapshots beneath them, what is listening, and — the part no existing tool answered — which datasets belong to no jail.
What running it against a real host taught it
The first version of this crate, driven only by fixtures, reported 22 orphaned datasets on a live FreeBSD 13.2 host. One was real — a 7 GB dataset whose jail had not existed for years. Of the rest, five were the boot pool's own system datasets and fourteen held virtual machine disks, two of them belonging to running virtual machines. An earlier claim that the question was worth "roughly 110 GB" came from that bad reading and is false; the interesting finding is the other one, that most of a jail host's unexplained space is usually explained, and a tool that cannot say by what will recommend deleting a running machine.
That is why a reconciliation here reports three things and not one: what is wrong (Disagreement), what is claimed by something other than a jail (DatasetClaim), and what it could not classify at all (Reconciliation::unclassified). An empty orphan list must never mean both "nothing to reclaim" and "I could not read this host".
Why the parsing lives here and the running does not
Every reader in this crate takes text and returns data. Nothing here runs a command by itself; a CommandRunner is passed in, and its subprocess calls use strict argument vectors — never a shell string. That rule is inherited from operations-host-inventory deliberately: three separate failures in one session came from shell strings being re-split by an intermediate shell, including a ps -axo user=,rss=,command= that silently returned one column because = swallowed the rest of the argument as a header. Argument vectors make that class impossible.
Capabilities
crate root
FreeBSD jail estate inventory: what a host actually holds, and where its own records
| Item |
|---|
fn strip_ansi(text : & str) -> String |
EstateError
FreeBSD jail estate inventory: what a host actually holds, and where its own records
| Item |
|---|
pub enum EstateError |
Result
FreeBSD jail estate inventory: what a host actually holds, and where its own records
| Item |
|---|
pub type Result<T>: std::result::Result <T, EstateError> |
datasets (other)
ZFS datasets and snapshots, as zfs list reports them.
| Item |
|---|
pub const ZFS_COLUMNS: & str |
Dataset
ZFS datasets and snapshots, as zfs list reports them.
| Item |
|---|
pub struct Dataset |
Dataset :: fn used_by_snapshots(& self) -> Option <& str> |
fn parse(output : & str) -> Result <Vec <Dataset>> |
fn detect <R : CommandRunner>(runner : & R) -> Result <Vec <Dataset>> |
Snapshot
ZFS datasets and snapshots, as zfs list reports them.
| Item |
|---|
pub struct Snapshot |
fn parse_snapshots(output : & str) -> Result <Vec <Snapshot>> |
jails (other)
The jails a host is running, as jls reports them.
| Item |
|---|
pub const JLS_COLUMNS: & str; 5 |
ConfiguredJails
The jails a host is running, as jls reports them.
| Item |
|---|
pub struct ConfiguredJails |
ConfiguredJails :: fn from_names <I, S>(names : I) -> Self where I : IntoIterator <Item = S>, S : Into <String>, |
ConfiguredJails :: fn contains(& self, name : & str) -> bool |
ConfiguredJails :: fn names(& self) -> & String |
ConfiguredJails :: fn len(& self) -> usize |
ConfiguredJails :: fn is_empty(& self) -> bool |
fn configured <R : CommandRunner>(runner : & R) -> Result <ConfiguredJails> |
Jail
The jails a host is running, as jls reports them.
| Item |
|---|
pub struct Jail |
fn parse(output : & str) -> Result <Vec <Jail>> |
fn detect <R : CommandRunner>(runner : & R) -> Result <Vec <Jail>> |
Listener
What is listening, as sockstat reports it.
| Item |
|---|
pub struct Listener |
fn parse(output : & str) -> Result <Vec <Listener>> |
fn detect <R : CommandRunner>(runner : & R, jid : & str) -> Result <Vec <Listener>> |
reconcile (other)
Where a host's records and its disks disagree — and, just as importantly, what it could
| Item |
|---|
fn reconcile(estate : & Estate, configured : & ConfiguredJails, layout : & JailLayout,) -> Reconciliation |
Claim
Where a host's records and its disks disagree — and, just as importantly, what it could
| Item |
|---|
pub enum Claim |
DatasetClaim
Where a host's records and its disks disagree — and, just as importantly, what it could
| Item |
|---|
pub struct DatasetClaim |
Disagreement
Where a host's records and its disks disagree — and, just as importantly, what it could
| Item |
|---|
pub enum Disagreement |
Estate
Where a host's records and its disks disagree — and, just as importantly, what it could
| Item |
|---|
pub struct Estate |
JailLayout
Where a host's records and its disks disagree — and, just as importantly, what it could
| Item |
|---|
pub struct JailLayout |
JailLayout :: fn default() -> Self |
Reconciliation
Where a host's records and its disks disagree — and, just as importantly, what it could
| Item |
|---|
pub struct Reconciliation |
testkit (other)
Fixtures captured from a real FreeBSD 13.2 jail host on 2026-09-11.
| Item |
|---|
pub const JLS: & str |
pub const ZFS_LIST: & str |
pub const ZFS_SNAPSHOTS: & str |
pub const SOCKSTAT: & str |
pub const CONFIGURED: & str; 5 |
How to use it
From `examples/estate-report.rs`:
use std::{fs, process};
use operations_jail_inventory::{
datasets, jails,
reconcile::{self, Claim, Disagreement, Estate, JailLayout},
strip_ansi, ConfiguredJails,
};
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let [jls_path, zfs_path, configured_path] = match args.as_slice() {
[a, b, c] => [a, b, c],
_ => {
eprintln!("usage: estate-report <jls.txt> <zfs-list.txt> <configured.txt>");
process::exit(2);
}
};
Module structure
operations_jail_inventory
datasetsjailslistenersreconciletestkit
flowchart TD n_operations_jail_inventory["operations_jail_inventory"] n_operations_jail_inventory --> n_datasets["datasets"] n_operations_jail_inventory --> n_jails["jails"] n_operations_jail_inventory --> n_listeners["listeners"] n_operations_jail_inventory --> n_reconcile["reconcile"] n_operations_jail_inventory --> n_testkit["testkit"]
Public surface
`crate root`
| Item | What it is |
|---|---|
pub enum EstateError | Reading an estate can fail in ways worth telling apart: the command was refused, or it answered in a shape we do not understand |
pub type Result<T>: std::result::Result <T, EstateError> | The result of reading one part of an estate. |
fn strip_ansi(text : & str) -> String | Remove ANSI colour escapes from captured output |
`datasets`
| Item | What it is |
|---|---|
pub struct Dataset | One ZFS dataset. |
pub struct Snapshot | One snapshot. |
Dataset :: fn used_by_snapshots(& self) -> Option <& str> | Space returned if every snapshot of this dataset were destroyed |
pub const ZFS_COLUMNS: & str | The columns this crate asks zfs list for. |
fn parse(output : & str) -> Result <Vec <Dataset>> | Parse zfs list -H -o name,used,mountpoint: tab-separated, no header. |
fn parse_snapshots(output : & str) -> Result <Vec <Snapshot>> | Parse zfs list -H -t snapshot -o name,used. |
fn detect <R : CommandRunner>(runner : & R) -> Result <Vec <Dataset>> | Read the datasets from a host. |
`jails`
| Item | What it is |
|---|---|
pub struct Jail | One running jail. |
pub const JLS_COLUMNS: & str; 5 | The column order this crate asks jls for |
fn parse(output : & str) -> Result <Vec <Jail>> | Parse jls -h jid name path host.hostname ip4.addr: a header line naming the columns, then one row per jail |
pub struct ConfiguredJails | The jails configured on one host, running or not |
ConfiguredJails :: fn from_names <I, S>(names : I) -> Self where I : IntoIterator <Item = S>, S : Into <String>, | Build from names the caller vouches for as belonging to one host |
ConfiguredJails :: fn contains(& self, name : & str) -> bool | Whether this host configures a jail of that name. |
ConfiguredJails :: fn names(& self) -> & String | The names, in the order the host gave them. |
ConfiguredJails :: fn len(& self) -> usize | How many jails this host configures. |
ConfiguredJails :: fn is_empty(& self) -> bool | Whether the host configures no jails at all. |
fn configured <R : CommandRunner>(runner : & R) -> Result <ConfiguredJails> | Read the jails configured on this host from cbsd |
fn detect <R : CommandRunner>(runner : & R) -> Result <Vec <Jail>> | Read the running jails from a host. |
`listeners`
| Item | What it is |
|---|---|
pub struct Listener | One listening socket. |
fn parse(output : & str) -> Result <Vec <Listener>> | Parse sockstat -j <jid> -l: a header line, then one row per socket |
fn detect <R : CommandRunner>(runner : & R, jid : & str) -> Result <Vec <Listener>> | Read the listening sockets of one jail. |
`reconcile`
| Item | What it is |
|---|---|
pub struct Estate | One host's estate, as read. |
pub struct JailLayout | How a host lays its jails out on disk |
JailLayout :: fn default() -> Self | — |
pub enum Disagreement | A problem |
pub enum Claim | What claims a dataset, when the claimant is not a jail |
pub struct DatasetClaim | One dataset and what claims it. |
pub struct Reconciliation | The whole answer: problems, claims, and what could not be read. |
fn reconcile(estate : & Estate, configured : & ConfiguredJails, layout : & JailLayout,) -> Reconciliation | Compare the jails against the datasets and report everything found |
`testkit`
| Item | What it is |
|---|---|
pub const JLS: & str | jls -h jid name path host.hostname ip4.addr |
pub const ZFS_LIST: & str | zfs list -H -o name,used,mountpoint, tab-separated |
pub const ZFS_SNAPSHOTS: & str | zfs list -H -t snapshot -o name,used |
pub const SOCKSTAT: & str | sockstat -j 55 -l — the wiki jail, after its httpd was started. |
pub const CONFIGURED: & str; 5 | The jails cbsd has configured, including stopped ones |
Re-exports. Exported here, defined elsewhere.
| Export | Defined in |
|---|---|
Listener | listeners::Listener |
{Claim,DatasetClaim,Disagreement,Estate,JailLayout,Reconciliation} | reconcile::{Claim,DatasetClaim,Disagreement,Estate,JailLayout,Reconciliation} |
{ConfiguredJails,Jail} | jails::{ConfiguredJails,Jail} |
{Dataset,Snapshot} | datasets::{Dataset,Snapshot} |
Boundary
Depends on no other workspace tier.
Shares tier operations with 42 other crates: operations-approval-workflow, operations-assessments, operations-block-imaging, operations-boot-media, operations-browser-agent-worker, operations-camera-discovery, operations-camera-liveview, operations-camera-registry, … (42 total).
_What this crate deliberately does NOT own is a judgment. No committed registry records one for it, so none is stated here._
Where it sits
| Tier (ontology) | operations |
| Architectural role (taxonomy) | infrastructure adapter |
| Location | crates/operations/jail-inventory |
| Vocabulary in force (lexicon) | current |
Dependencies
Runtime, in this workspace.
| Crate | Tier | Optional | Only on |
|---|---|---|---|
| `operations-host-inventory` | operations | no | always |
Runtime, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
serde | ^1 | derive | no | always |
thiserror | ^2 | — | no | always |
Development, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
serde_json | ^1 | — | no | always |
Build. None.
Depended on by. Nothing in this workspace.
Signal flow — what reaches this crate, and what it reaches.
flowchart LR SELF["operations-jail-inventory"] SELF -->|runtime| n_operations_host_inventory["operations-host-inventory"] classDef self fill:#1f883d,stroke:#1f883d,color:#fff; class SELF self;
Feature flags
No Cargo features are defined: every capability is unconditional, so no consumer can receive a half-wired crate.
Targets
| Kind | Name | Source |
|---|---|---|
| example | estate-report | `examples/estate-report.rs` |
| lib | operations_jail_inventory | `src/lib.rs` |
| test | estate | `tests/estate.rs` |
Error model
| Error type | Named by |
|---|---|
EstateError | Result |
Operational characteristics
| Property | Evidence |
|---|---|
| async public surface | none detected |
| async runtime | none detected |
| database access | none detected |
| network I/O | none detected |
| unsafe code | none detected |
| environment variables | none detected |
No unsafe block, unsafe fn, unsafe impl or unsafe trait was found by the parser anywhere in this crate's source.
Configuration
No environment variable is read with a literal name anywhere in this crate. A variable whose key is computed at run time cannot be listed here, and is not claimed to be absent.
Related capabilities
No workspace crate depends on this one.
Verification
| Kind | Count |
|---|---|
| Unit tests | 0 |
| Integration tests | 24 |
| Examples | 1 |
| Doctests | 0 |
Evidence by module. How often each public module is named by something executable.
| Module | Tests | Examples | Consumers |
|---|---|---|---|
crate root | 3 | 2 | 0 |
datasets | 6 | 2 | 0 |
jails | 6 | 4 | 0 |
listeners | 3 | 1 | 0 |
reconcile | 7 | 5 | 0 |
testkit | 5 | 1 | 0 |
What the tests establish, by name:
a_configured_jail_with_no_dataset_is_reported—tests/estate.rsa_dataset_holding_a_vm_disk_is_claimed_not_orphaned—tests/estate.rsa_dataset_whose_jail_is_not_configured_is_an_orphan—tests/estate.rsa_dataset_with_no_mountpoint_is_read_as_having_none—tests/estate.rsa_host_using_another_layout_reports_unclassified_not_a_clean_estate—tests/estate.rsa_jail_with_no_address_reports_none_not_a_dash—tests/estate.rsa_leading_blank_line_does_not_turn_the_header_into_a_listener—tests/estate.rsa_refused_command_is_a_command_failure_not_an_empty_estate—tests/estate.rsa_running_jail_without_an_address_is_reported—tests/estate.rsa_stopped_jail_is_not_an_orphan—tests/estate.rsa_truncated_jls_row_is_refused_by_line_number—tests/estate.rsa_vm_disk_is_recognised_by_its_child_even_under_a_jail_mountpoint—tests/estate.rsanother_pools_system_datasets_are_not_jail_orphans—tests/estate.rsconfigured_jail_names_are_stripped_of_the_colour_cbsd_emits_when_piped—tests/estate.rsdataset_detection_passes_each_flag_as_its_own_argument—tests/estate.rsdatasets_are_split_on_tabs_and_keep_the_hosts_own_units—tests/estate.rsjail_detection_passes_each_column_as_its_own_argument—tests/estate.rsjails_are_read_by_header_position_not_field_order—tests/estate.rsjls_output_without_a_header_is_unreadable_not_empty—tests/estate.rslistener_detection_passes_each_flag_as_its_own_argument—tests/estate.rslisteners_skip_the_header_and_read_the_local_address—tests/estate.rssnapshot_rows_are_parsed_and_their_used_is_per_snapshot_only—tests/estate.rssockstat_output_without_a_header_is_unreadable—tests/estate.rszfs_output_with_no_rows_is_unreadable_not_an_empty_estate—tests/estate.rs
Documentation coverage
| Measure | Documented | Total |
|---|---|---|
| Public items with rustdoc | 36 | 37 |
Public modules with a //! block | 5 | 5 |
pie showData
title Public items with rustdoc
"Documented" : 36
"No rustdoc detected" : 1
Metrics
| Metric | Value |
|---|---|
| Rust source files | 6 |
| Source lines | 808 |
| Code lines | 501 |
| Public API items | 37 |
| Public modules | 5 |
| Tests | 24 |
| Examples | 1 |
| Cargo features | 0 |
| Direct runtime dependencies | 3 |
| Workspace reverse dependencies | 0 |
pie showData
title Public API by kind
"constant" : 7
"enum" : 3
"function" : 10
"method" : 7
"struct" : 9
"type alias" : 1
pie showData
title Rust source composition
"Code" : 501
"Blank or comment" : 307
Generation
Rendered by tools-corpus corpus readme from repository evidence alone, renderer schema 2, lexicon current. No model, network service or database was consulted. Regenerate with tools-corpus corpus readme --write; verify with --check.