A lightweight NAS management UI written in Go — built for Linux and FreeBSD, designed to stay out of the way of a vanilla system.
Pre-1.0 notice: dumpstore is under active development. Until v1.0, any release may introduce breaking changes to the API, config format, or behaviour. Use in production at your own risk.
I run a Kobol Helios64 as my home NAS — a five-bay ARM board that deserves better than the software ecosystem currently offers it. The existing storage UIs I tried were either too heavy, too opinionated about the underlying distribution, or simply unmaintained. None of them gave me a clean, no-nonsense window into my ZFS pools without pulling in a container runtime, a database, or a Node.js server alongside them.
What I wanted was simple: observe and manage my storage from a browser, on a machine that stays as close to a vanilla Linux or FreeBSD installation as possible. No agents, no daemons-within-daemons, no frameworks that outlive their welcome. Just a single compiled binary, some Ansible playbooks, and a handful of static files.
Where dumpstore does manage a service — Samba, NFS, iSCSI — it takes full, explicit ownership of that service's config. No half-measures: the config file is rendered from a template on every write. If you need to run those services alongside another management tool, dumpstore is not the right fit for that service.
dumpstore started as exactly that — a thin read-only dashboard — and is growing deliberately from there. The roadmap includes everything a real NAS UI needs: SMB/NFS share management, fine-grained permissions, and ZFS send/receive. Each feature will follow the same philosophy: keep the host clean, keep the code auditable, and let the operating system do the heavy lifting.
That philosophy has a hard edge: dumpstore manages ZFS and sharing (SMB/NFS/iSCSI/wsdd, plus the local users and permissions they need) and only integrates with everything else. Identity servers, monitoring stacks, UPS daemons, notification systems — where a mature tool already owns a domain, dumpstore reads, displays, and links out instead of rebuilding it. A feature either makes dumpstore better at running ZFS and shares, or it belongs in another tool.
If you run a Helios64, an old server, or any ZFS box where you care about what is actually installed on it, this might be the tool for you.
- System info — hostname, OS, kernel, CPU, uptime, load averages, process stats; the Installed Software card distinguishes required tools (ZFS, Ansible, Python) from optional ones and names the feature each optional tool unlocks — a missing required tool is flagged with a red badge; platform warnings surface common misconfigurations (e.g. FreeBSD: ZFS kernel module not loaded,
zfs_enablenot set) - Pool overview — health badges, usage bars, fragmentation, deduplication ratio, vdev tree
- Pool scrub management — trigger and cancel scrubs; last scrub time, status, and progress per pool; configure periodic scrub schedules (Linux:
zfsutils-linux; FreeBSD:periodic.conf) - Pool lifecycle — create pools (single/mirror/raidz1-3/draid1-3) with an unused-device picker, optional ashift/compression, and confirm-by-typing; import exported pools (with force option); export pools from the pool card
- Pool expansion — add data vdevs (confirm-by-typing — irreversible), L2ARC cache devices, SLOG log devices (optionally mirrored), and hot spares; remove cache/log/spare devices from the vdev tree
- Drive replacement & resilver monitoring — replace a (faulted) device from the vdev tree with a picker of unused block devices (
zpool replace); take devices offline / bring them online for maintenance; resilver progress bar with live percentage and a completion notification; faulted/degraded devices highlighted in the vdev tree - I/O statistics — live read/write IOPS and bandwidth per pool
- Disk health — S.M.A.R.T. data per drive (temperature, power-on hours, reallocated sectors, pending sectors, uncorrectable errors)
- Dataset browser — datasets grouped into per-pool sections (pool health badge + capacity bar on the header row), depth-indented collapsible tree, per-row usage bar against quota, passive status chips (NFS/SMB/iSCSI/auto-snap/ACL), quick actions on hover
- Dataset creation — create filesystems and volumes with any combination of ZFS properties
- Dataset detail drawer — click any row for a slide-in panel grouping everything by concern: inline property editing (inherited values shown, set or inherit, single Apply), sharing (NFS/SMB/iSCSI), permissions (ACL/chown), snapshots, and a separated danger zone (rename/delete)
- Dataset rewrite — apply updated properties (compression, checksum, copies) to already-stored data via
zfs rewritefrom the dataset detail drawer; recurse/skip-snapshot-shared/skip-clone-shared options; runs as a background job in the Jobs tab - Dataset deletion — destroy datasets and volumes with recursive option and confirm-by-typing dialog
- Per-user/group space & quotas — see who consumes space on a filesystem (
zfs userspace/zfs groupspace) and set or removeuserquota@/groupquota@limits inline - Snapshot management — snapshots grouped into collapsible per-dataset sections (count, total size, newest age on the header); rows show relative age; create (recursive) and delete with styled confirm dialogs; per-group and global bulk select feeding a batch-delete toolbar
- Dataset rename — rename a dataset or volume in place (same-parent constraint)
- Snapshot clone — create a new writable dataset from an existing snapshot
- Snapshot diff — see what changed between a snapshot and a later snapshot or the live filesystem (
zfs diff); color-coded change types with a path filter - Snapshot send/receive — replicate a snapshot to another local pool or to a remote host over SSH; runs as a background job tracked in the Jobs tab (status, runtime, output tails, cancel); optional incremental (
-i) using a prior snapshot of the same dataset and--rawfor encrypted datasets; SSH keys must be pre-configured for the dumpstore service account - Scheduled replication — cron-scheduled replication tasks (5-field syntax, 1-minute resolution); each fire snapshots the source as
dumpstore-repl-<UTC>, places a hold for the duration of the transfer, picks the most recent commondumpstore-repl-*for an incremental send, dispatches the pipeline via the jobs runner, releases the hold on completion, and prunes destination replication snapshots to a configurable retention count; per-task run history with "Run now" override; supports local and remote (user@host) targets - Background jobs — long-running data-plane operations (currently snapshot send/receive) run outside Ansible via the jobs manager; each runs in its own process group with SIGTERM→SIGKILL cancel; status persists across service restarts (interrupted jobs are surfaced as such); live updates via SSE
- Auto-snapshot scheduling (native) — dumpstore executes
com.sun:auto-snapshot:*snapshots itself via the built-in 5-field cron scheduler. Snapshots are named to match the legacyzfs-auto-snap_<bucket>-…convention so existing snapshots are recognised for retention pruning, and property inheritance is honoured correctly viazfs get(no more FreeBSDzfstoolsinheritance bug). One-click takeover from / release back to the OS daemon (zfs-auto-snapshoton Linux,zfstoolson FreeBSD) - User management — list, create, edit (shell, password, primary/supplementary groups, home directory, SSH authorized keys, Samba password sync), and delete local users; system users (uid < 1000) hidden by default with a toggle to reveal them
- Group management — list, create, edit (name, GID, members), and delete local groups; system groups hidden by default with the same toggle
- NFS share management — enable, configure, and disable NFS sharing per dataset via the ZFS
sharenfsproperty; cross-platform (Linux and FreeBSD) - SMB share management — create and remove Samba usershares per dataset via
net usershare; manage Samba users (add/remove fromsmbpasswd); dumpstore takes full ownership ofsmb.conf/smb4.confand renders it from a template on every write; all sub-features gated behind one-time initialisation (POST /api/smb/init); cross-platform (Linux and FreeBSD) - SMB home shares — enable and configure the Samba
[homes]section; configurable base path (pick a ZFS dataset or specify a custom path), browseable, read only, create mask, and directory mask; base directory is created automatically on apply - Time Machine shares — create Samba shares configured as macOS Time Machine backup targets using
vfs_fruit; multiple named shares each backed by a different ZFS dataset; configurable max size quota and valid users; target directory created automatically on apply - iSCSI target management — expose ZFS volumes as iSCSI targets via
targetcli/LIO on Linux orctldon FreeBSD; per-zvol dialog with IQN (auto-generated, editable), portal IP/port, auth mode (None/CHAP), and initiator ACL list - ACL management — view, add, and remove POSIX ACL entries (
getfacl/setfacl, requiresaclpackage) and NFSv4 ACL entries (nfs4_getfacl/nfs4_setfacl, requiresnfs4-acl-tools) per dataset; setting an ACL entry automatically sets the correctacltypeZFS property; one-click enable for datasets withacltype=off; recursive apply supported for POSIX - Live updates — Server-Sent Events push pool, dataset, snapshot, I/O, user and group changes; server polls every 10 s and pushes only on change; falls back to 30 s REST polling if SSE is unavailable
- Prometheus metrics —
GET /metricsexposes Go runtime and process stats, HTTP request counters and latency histograms (http_requests_total,http_request_duration_seconds), and Ansible playbook metrics (ansible_runs_total,ansible_run_duration_seconds) - OpenTelemetry export — set
OTEL_EXPORTER_OTLP_ENDPOINTand dumpstore pushes traces (per-request root spans, Ansible/ops child spans, background-job/replication/autosnap spans), logs (the journald stream withtrace_idcorrelation), and Go runtime metrics to any OTLP collector; no-op without the env var - Request ID correlation — every request gets a unique
req_idcarried on all log lines for that request; readsX-Request-IDfrom upstream proxies (nginx, Traefik) and echoes it back on the response - Audit logging — every mutating operation (dataset/snapshot/user/group/ACL/SMB/iSCSI/scrub create, modify, destroy) emits a structured
slogaudit line withremote_ip,action,target, andoutcome(ok/err);req_idis included automatically
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
┌─────────────────────────────────────────────────────────────────────┐
│ Browser (vanilla JS SPA) │
│ state + reactive store → subscribe(keys, renderFn) │
│ storeSet(key, val) auto-dispatches subscribed renderers │
│ │
│ ┌─ boot ──────────────────────────────────────────────────────┐ │
│ │ loadAll() → 14 parallel REST fetches (initial paint) │ │
│ │ storeBatch() coalesces updates; each render fires once │ │
│ │ startSSE() → EventSource /api/events?topics=… │ │
│ │ on message: storeSet(key, data) → auto render │ │
│ │ on close: fallback to setInterval(loadAll, 30 000) │ │
│ │ + retry SSE after 5 s │ │
│ └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────────────┘
│ HTTP :8080 (REST + SSE)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ main.go │
│ • flag: -addr -dir -debug │
│ • startup: checks ansible-playbook in PATH, │
│ playbooks/ and static/ dirs exist │
│ • signal.NotifyContext → graceful shutdown on SIGTERM/SIGINT │
│ • logging.RequestLogger middleware (method/path/status/ms/req_id) │
│ ↳ reads X-Request-ID from proxy, generates one if absent, │
│ echoes it back on the response, stores in ctx for slog │
│ • GET / → http.FileServer (static/) │
│ • /api/* → api.Handler │
└───────────────────┬─────────────────────────────────────────────────┘
│
┌─────────────┼───────────────────────────────┐
│ │ │
│ ┌───────┴──────────────────┐ │
│ │ internal/broker │ │
│ │ │ │
│ │ Broker — pub/sub core │◄── StartPoller() goroutine
│ │ Subscribe(topic) │ polls ZFS + users/groups every 10 s
│ │ Publish(topic, data) │ publishes only on change
│ │ Unsubscribe(topic,ch) │ (JSON equality check)
│ │ │
│ │ GET /api/events │──► streams SSE to browsers
│ │ ?topics=pool.query,… │ fan-in per-topic channels
│ └──────────────────────────┘
│
├─── READ requests WRITE requests ───────────┐
│ pools, datasets, snapshots, create / edit / destroy │
│ iostat, status, props, datasets, snapshots, │
│ sysinfo, SMART, metrics, users, groups, ACLs, │
│ users, groups, ACLs, SMB users/shares/config, │
│ SMB users/shares/homes, dataset chown, scrub, │
│ iSCSI targets, iSCSI targets, │
│ Time Machine shares SMB homes config, │
│ Time Machine shares │
│ │
▼ ▼
┌───────────────────────┐ ┌────────────────────────────┐
│ internal/zfs/zfs.go │ │ internal/ansible/runner.go │
│ internal/system/ │ │ │
│ internal/smart/ │ │ Run(playbook, extraVars) │
│ internal/iscsi/ │ │ │
│ │ │ │
│ ListPools() │ │ exec: ansible-playbook │
│ ListDatasets() │ │ -i inventory/localhost │
│ ListSnapshots() │ │ --extra-vars '{...}' │
│ IOStats() │ │ env: ANSIBLE_STDOUT_ │
│ GetDatasetProps() │ │ CALLBACK=ndjson │
│ GetDatasetACL() │ │ │
│ GetMountpointOwner() │ │ │
│ PoolStatuses() │ │ parse ndjson output │
│ Version() │ │ → []TaskStep │
│ system.Get() │ │ streams live via SSE │
│ system.ListUsers() │ │ │
│ system.ListGroups() │ │ │
│ system.ListSamba*() │ │ │
│ smb.ParseSMBConfig() │ │ │
│ smart.Collect() │ │ │
│ iscsi.ListTargets() │ │ │
│ │ │ │
│ exec: zpool / zfs / │ │ │
│ smartctl / sysctl / │ │ │
│ pdbedit / net │ │ │
│ (no Python startup) │ │ │
└──────────┬────────────┘ └────────────┬───────────────┘
│ │
▼ ▼
ZFS kernel playbooks/*.yml
subsystem ┌──────────────────────┐
│ targets: localhost │
│ gather_facts: false │
│ 1. assert vars │
│ 2. mutating command │
└──────────────────────┘
When dumpstore manages a service, it takes full ownership of that service's config file. The config is rendered from a Go template on every write — no block-patching, no lineinfile, no partial edits. If you manually edit a managed config file, dumpstore will overwrite it on the next write operation.
The rule is binary: own it completely, or don't touch it at all.
| Service | Owned? | Config file | Restart mechanism |
|---|---|---|---|
| Samba | ✅ full | /etc/samba/smb.conf / /usr/local/etc/smb4.conf |
systemctl restart smbd / service samba_server restart |
| NFS | ✅ via ZFS | ZFS sharenfs property (ZFS manages /etc/exports) |
automatic on zfs set |
| iSCSI | ✅ via CLI | targetcli saveconfig / /etc/ctl.conf |
targetcli / service ctld restart |
| TLS | ✅ full | dumpstore.conf cert fields |
dumpstore reload |
| Users / Groups | — | OS is source of truth | n/a |
| ZFS datasets | — | ZFS kernel properties | n/a |
For config-owning services the write path is extended:
playbooks/smb_apply.yml (example)
┌──────────────────────────────────┐
│ targets: localhost │
│ gather_facts: false │
│ 1. assert vars │
│ 2. create referenced dirs │
│ 3. render full config (template)│
│ 4. restart service │
└──────────────────────────────────┘
Sub-features (shares, home dirs, Time Machine targets) are gated behind an init gate — they are disabled until the service has been bootstrapped with POST /api/smb/init (or equivalent). This prevents partial config states.
| Lane | Mechanism | Used for |
|---|---|---|
| Reads | exec.Command(zpool/zfs/smartctl) |
Everything GET — fast, no Python startup |
| Single-command writes | internal/ops — in-process argv exec, no shell |
zpool scrub/replace/add/…, zfs snapshot/destroy/clone, quotas — validated in Go, ~ms latency |
| Config-file / OS-resource writes | exec.Command(ansible-playbook) |
smb.conf rendering, users/groups, TLS, ACLs, dataset property playbooks — where idempotent orchestration earns its keep |
| Long-running data-plane | internal/jobs — background process groups |
zfs send | recv, zfs rewrite — hours-long, 202 + job polling |
Both write lanes report the same tasks: [...] step shape and stream live progress over the ansible.progress SSE topic, so the op-log dialog in the UI is identical regardless of lane. Ansible (and its Python dependency) is required only for the config-file / OS-resource features.
Browser
│ POST /api/snapshots {"dataset":"tank/data","snapname":"bkp"}
▼
zfs_handlers.go: createSnapshot()
│ validate input (names, labels — Go is the single validation point)
│ build argv: ["zfs", "snapshot", "tank/data@bkp"]
▼
internal/ops: Run(steps) ← in-process, no shell, no Python
│ exec zfs snapshot tank/data@bkp
│ step result → ansible.TaskStep{name, status, msg}
│ each step → ansible.progress SSE topic (live op-log)
▼
handlers.go: return 201 {"snapshot":"tank/data@bkp","tasks":[...]}
▼
Browser: showOpLog() renders task steps in modal
Config-file writes (e.g. SMB shares) take the same shape but execute via
ansible-playbook with the ndjson callback instead of internal/ops.
GET /api/sysinfo → /proc/*, sysctl (direct)
GET /api/network → net.Interfaces() (direct)
GET /api/version → zpool version (direct)
GET /api/pools → zpool list (direct)
GET /api/poolstatus → zpool status (direct)
GET /api/datasets → zfs list (direct)
GET /api/dataset-props/{n} → zfs get (direct)
GET /api/snapshots → zfs list -t snap (direct)
GET /api/iostat → zpool iostat (direct)
GET /api/smart → smartctl (direct)
GET /metrics → Prometheus text (direct)
GET /api/events → SSE stream (broker)
POST /api/datasets → zfs_dataset_create.yml (ansible)
PATCH /api/datasets/{n} → zfs_dataset_set.yml (ansible)
DELETE /api/datasets/{n} → zfs_dataset_destroy.yml (ansible)
POST /api/snapshots → zfs_snapshot_create.yml (ansible)
DELETE /api/snapshots/{n} → zfs_snapshot_destroy.yml (ansible)
POST /api/snapshots/clone → zfs_snapshot_clone.yml (ansible)
POST /api/snapshots/send → internal/jobs (direct exec, fire-and-forget)
GET /api/jobs → list of background jobs (direct)
GET /api/jobs/{id} → single job status (direct)
POST /api/jobs/{id}/cancel → SIGTERM → SIGKILL grace (direct)
DELETE /api/jobs/{id} → remove terminal job (direct)
GET /api/auto-snapshot/status → ownership state (OS daemon vs dumpstore) (direct)
POST /api/auto-snapshot/takeover → auto_snapshot_takeover.yml (ansible)
POST /api/auto-snapshot/release → auto_snapshot_release.yml (ansible)
GET /api/replication → list scheduled replication tasks (direct)
POST /api/replication → create replication task (direct)
PATCH /api/replication/{id} → update replication task (direct)
DELETE /api/replication/{id} → delete replication task (direct)
POST /api/replication/{id}/run → fire immediately, returns job_id (direct → internal/jobs)
GET /api/replication/{id}/history → recent RunRecord entries (direct)
GET /api/users → /etc/passwd (direct)
POST /api/users → user_create.yml (ansible)
PUT /api/users/{name} → user_modify.yml (ansible)
DELETE /api/users/{name} → user_delete.yml (ansible)
GET /api/users/{name}/sshkeys → ~/.ssh/authorized_keys (direct)
POST /api/users/{name}/sshkeys → user_ssh_key_add.yml (ansible)
DELETE /api/users/{name}/sshkeys → user_ssh_key_remove.yml (ansible)
GET /api/groups → /etc/group (direct)
POST /api/groups → group_create.yml (ansible)
PUT /api/groups/{name} → group_modify.yml (ansible)
DELETE /api/groups/{name} → group_delete.yml (ansible)
GET /api/chown/{dataset} → stat(mountpoint) (direct)
POST /api/chown/{dataset} → dataset_chown.yml (ansible)
GET /api/acl-status → getfacl / acltype (direct)
GET /api/acl/{dataset} → getfacl / nfs4_getfacl (direct)
POST /api/acl/{dataset} → acl_set_posix.yml (ansible)
acl_set_nfs4.yml
DELETE /api/acl/{dataset} → acl_remove_posix.yml (ansible)
acl_remove_nfs4.yml
GET /api/smb-shares → net usershare list (direct)
GET /api/smb-users → pdbedit -L (direct)
POST /api/smb-share/{ds} → smb_usershare_set.yml (ansible)
DELETE /api/smb-share/{ds} → smb_usershare_unset.yml (ansible)
POST /api/smb-users/{name} → smb_user_add.yml (ansible)
DELETE /api/smb-users/{name} → smb_user_remove.yml (ansible)
GET /api/smb/status → smb.IsInitialized() (direct)
POST /api/smb/init → smb_init.yml (ansible)
GET /api/smb/homes → smb.ParseSMBConfig() (direct)
POST /api/smb/homes → smb_apply.yml (ansible, full config render)
DELETE /api/smb/homes → smb_apply.yml (ansible, full config render)
GET /api/smb/timemachine → smb.ParseSMBConfig() (direct)
POST /api/smb/timemachine → smb_apply.yml (ansible, full config render)
DELETE /api/smb/timemachine/{n}→ smb_apply.yml (ansible, full config render)
GET /api/auto-snapshot/{ds} → zfs get com.sun:auto-snapshot* (direct)
PUT /api/auto-snapshot/{ds} → zfs_autosnap_set.yml (ansible)
GET /api/iscsi-targets → parse targetcli saveconfig.json or /etc/ctl.conf (direct)
POST /api/iscsi-targets → iscsi_target_create.yml / iscsi_target_create_freebsd.yml (ansible)
DELETE /api/iscsi-targets → iscsi_target_delete.yml / iscsi_target_delete_freebsd.yml (ansible)
dumpstore has built-in session-based authentication.
First-time setup: install.sh prompts for a password before starting the service. On subsequent upgrades the prompt is skipped if a password is already set.
To set or reset the password manually:
# Linux
sudo /usr/local/lib/dumpstore/dumpstore --set-password --config /etc/dumpstore/dumpstore.conf
sudo systemctl restart dumpstore
# FreeBSD
sudo /usr/local/lib/dumpstore/dumpstore --set-password --config /usr/local/etc/dumpstore/dumpstore.conf
sudo service dumpstore restartIf no password is configured the service starts but binds to 127.0.0.1 only with a warning in the logs.
Configuration (/etc/dumpstore/dumpstore.conf on Linux, /usr/local/etc/dumpstore/dumpstore.conf on FreeBSD, JSON):
{
"username": "admin",
"password_hash": "$2a$12$...",
"session_ttl": "24h",
"trusted_proxies": ["127.0.0.1/32"],
"unprotected_paths": ["/metrics"]
}Reverse proxy delegation: If you run dumpstore behind nginx, Caddy, or Authelia, configure the proxy's CIDR in trusted_proxies and set the X-Remote-User header from your SSO. dumpstore will accept that header as the authenticated identity without requiring a password login.
In-app settings: Username and password can be changed from the Authentication section at the top of the Users & Groups tab. Both operations go through Ansible and show the operation log.
dumpstore runs as root (required for ZFS). See SECURITY.md for notes on TLS and the recommended deployment topology.
Two complementary exports, both integrations with an external stack (dumpstore never embeds dashboards or alerting — see the scope boundary):
Prometheus (pull) — GET /metrics exposes Go runtime and process stats, HTTP request counters/latency histograms, and Ansible/ops run metrics. Always on, no configuration.
OpenTelemetry (push) — set the standard OTLP environment variables and dumpstore exports traces, logs, and runtime metrics to your collector (Alloy, Tempo/Loki/Mimir, Jaeger, …). Without them, every OTEL code path is a no-op.
# Linux (systemd): systemctl edit dumpstore, or uncomment the examples in the unit
Environment=OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy.example:4318
Environment=OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf # default; "grpc" also supported
Environment=OTEL_SERVICE_NAME=dumpstore # default
# FreeBSD (rc.d): rc.subr passes ${name}_env to the daemon
sysrc dumpstore_env="OTEL_EXPORTER_OTLP_ENDPOINT=http://alloy.example:4318"
service dumpstore restartOn Linux the log stream keeps its systemd journal integration (syslog <N> priority prefixes when attached to the journal); on FreeBSD the same records go to the daemon logfile (/var/log/dumpstore.log) in the plain format, unchanged from previous releases. The OTLP export behaves identically on both.
What gets exported when enabled:
- Traces — a root span per API request (named
METHOD /route/pattern, carrying the journaldreq_idfor two-way correlation); child spans for every Ansible playbook run (ansible.playbook, extra-var keys only — values may hold secrets) and every ops-lane command (ops.zfs snapshot, argv attached); root spans for background work — jobs (job.<type>, linked to the dispatching request's span), scheduled replication runs (replication.run), and auto-snapshot fires (autosnap.run). ZFS read calls are deliberately not traced (sub-10 ms, high volume). - Logs — the same records that go to journald, shipped over OTLP with
trace_id/span_idcorrelation. Logging is a single-producer pipeline: slog feeds the OTEL log SDK, whose journald exporter reproduces the classic output (identical format, syslog priority prefixes) while the OTLP branch exports the same stream. journald lines gaintrace_id/span_idtoo when a request context is present. If the collector is down, OTLP records buffer and drop in the background — journald output is never affected. - Metrics — Go runtime metrics (
contrib/instrumentation/runtime) andhttp.server.*metrics pushed via OTLP, alongside the unchanged Prometheus endpoint.
All standard OTEL SDK env vars are honored (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_TRACES_SAMPLER, OTEL_SDK_DISABLED, …); a client disconnect never cancels a mutating command — request contexts are used for span parenting only.
For OTLP-only setups, local logging can be switched off with -log-stdout=false — the journald/logfile branch is dropped from the pipeline and records go exclusively to the collector. With no OTLP endpoint configured this discards all logs, so dumpstore prints a warning to stderr at startup in that combination.
| Linux | FreeBSD | |
|---|---|---|
| ZFS | zfsutils-linux or equivalent |
built-in (zfsutils pkg for older releases) |
| Ansible | ansible package (Python 3) |
py311-ansible or equivalent |
| Service manager | systemd | rc.d (via daemon(8)) |
| S.M.A.R.T. (optional) | smartmontools |
smartmontools pkg |
| POSIX ACLs (optional) | acl pkg (getfacl/setfacl) |
py311-pylibacl or acl port |
| NFS sharing (optional) | nfs-kernel-server (Debian) or nfs-utils (RHEL/Fedora) |
built-in base system |
| SMB sharing (optional) | samba (smbd, net, pdbedit); for ZFS ACL passthrough via sharesmb also install samba-vfs-modules (Debian/Ubuntu) or samba-vfs (RHEL/Fedora) |
samba pkg |
| NFSv4 ACLs (optional) | nfs4-acl-tools pkg (nfs4_getfacl/nfs4_setfacl) |
nfs4-acl-tools port |
| iSCSI (optional) | targetcli-fb (targetcli) |
built-in ctld |
| TLS / ACME (optional) | openssl (usually pre-installed); lego for ACME |
same |
| Build | Go 1.22+ | Go 1.22+ |
Go and Ansible are the only hard requirements. ZFS must be available on the target machine — the server checks for zfs, zpool, and ansible-playbook in PATH at startup and refuses to start with a clear error if any is missing; the binary itself builds and runs on any platform. On FreeBSD, the System tab additionally warns when the ZFS kernel module is not loaded even though the userland tools are installed. The Installed Software card on the System tab shows which of the optional tools below are present and which feature each one unlocks.
The NFS server and ACL tools are optional — the relevant dialogs will show an error if the required tool is not installed. Install only what you need:
# Debian/Ubuntu — POSIX ACLs
apt install acl
# Debian/Ubuntu — NFS sharing
apt install nfs-kernel-server
systemctl enable --now nfs-server
# Debian/Ubuntu — NFSv4 ACLs
apt install nfs4-acl-tools
# RHEL/Fedora — NFS sharing
dnf install nfs-utils
systemctl enable --now nfs-server
# RHEL/Fedora — ACLs
dnf install acl nfs4-acl-tools
# Debian/Ubuntu — SMB sharing
apt install samba
# Then run the SMB setup from the dumpstore UI (Settings → Configure Samba)
# or manually: ansible-playbook playbooks/smb_setup.yml
# Debian/Ubuntu — iSCSI targets
apt install targetcli-fb
# RHEL/Fedora — iSCSI targets
dnf install targetcli
# FreeBSD — iSCSI targets (ctld is built-in, just enable the service)
sysrc ctld_enable=YES
service ctld start
# ACME cert issuance via Let's Encrypt (lego) — only needed if using --tls with ACME
# Debian/Ubuntu
apt install lego
# or download a release binary: https://github.com/go-acme/lego/releases
# RHEL/Fedora
dnf install lego
# FreeBSD
pkg install legoContributions are welcome. Please read CONTRIBUTING.md before opening a PR — it covers the no-external-dependencies rule, the read/write split convention, playbook and frontend standards, and the docs update requirement.
Bug reports and feature requests go through the issue tracker using the provided templates.
This project follows a Code of Conduct.
Releases are tagged with semver (v0.1.0, v0.2.0, …). The version is injected at build time via ldflags from git describe:
v0.1.0 ← exact tag
v0.1.0-3-gabcdef ← 3 commits after tag
v0.1.0-3-gabcdef-dirty ← uncommitted changes present
dev ← built outside git (no tags)
The version is exposed in:
./dumpstore -version— prints and exitsGET /api/sysinfo→app_versionfieldGET /metrics→dumpstore_build_info{version="..."}label- UI version bar (alongside the OpenZFS version)
Clone the repository and run install.sh as root. It checks prerequisites, builds the binary, installs everything to /usr/local/lib/dumpstore/, and registers the service.
git clone https://github.com/langerma/dumpstore.git
cd dumpstore
sudo ./install.shTo remove dumpstore completely:
sudo ./install.sh --uninstallmake install detects the OS automatically and registers the appropriate service.
# Tag a release (optional — omitting gives "dev" as version)
git tag v0.1.0
# Build and install
make build
sudo make installThe service will be available at http://localhost:8080.
The unit file is installed to /etc/systemd/system/dumpstore.service.
To change the listen address:
# Edit ExecStart in the unit file, then:
sudo systemctl daemon-reload && sudo systemctl restart dumpstoreThe rc script is installed to /usr/local/etc/rc.d/dumpstore. The installer runs sysrc dumpstore_enable=YES automatically.
To customise address or install path, add to /etc/rc.conf:
dumpstore_enable="YES"
dumpstore_addr=":9090"
dumpstore_dir="/usr/local/lib/dumpstore"
Then service dumpstore restart.
go build -o dumpstore .
sudo ./dumpstore -addr :8080 -dir .-dir must point to the directory that contains playbooks/ and static/. It defaults to the directory of the executable.
Run against fake CLI stubs on macOS or any machine without ZFS/Ansible:
make devdev/bin/ stubs intercept zfs, zpool, and ansible-playbook with static responses so the full UI renders and write dialogs show op-logs.
Spin up headless Lima VMs with ZFS and Ansible pre-provisioned. Requires Lima:
brew install lima # Homebrew
sudo port install lima # MacPorts
# or download from https://github.com/lima-vm/lima/releasesLinux (Ubuntu 24.04, port 8080):
make vm-linux-start # create + boot (first run provisions the VM, ~5 min)
make vm-linux-deploy # pack source, copy to VM, run make install natively
make vm-linux-ssh # open a shell inside the VM
make vm-linux-stop # suspend
make vm-linux-destroy # tear down completelyFreeBSD 15 (port 8081):
make vm-freebsd-start
make vm-freebsd-deploy
make vm-freebsd-ssh
make vm-freebsd-stop
make vm-freebsd-destroyBoth VMs run via QEMU (the Linux VM follows the host architecture; FreeBSD is arm64). Each gets a dedicated 10 GiB extra disk for ZFS (tank pool created at first boot) plus three 1 GiB scratch disks used by the integration tests. Deployment packs the source tree on the host, copies it into the VM, and runs make install natively — no cross-compilation. Port forwards are fixed: Linux on :8080, FreeBSD on :8081, so both can be up simultaneously. Default credentials: admin / admin.
With the Linux VM up and deployed, run the end-to-end suite from the host:
make test-integrationIt drives the deployed API over HTTP — auth, dataset/snapshot lifecycle, zfs diff, user quotas, send/recv jobs, and a full pool lifecycle (create, offline/online, zpool replace + resilver, spares, export/import) on the scratch disks. CI runs the same suite nightly and on PRs labeled run-integration (.github/workflows/integration-tests.yml). See tests/integration/README.md for configuration and for running it against the FreeBSD VM.
sudo make uninstall.
├── main.go # HTTP server, flag parsing, startup dependency checks
├── go.mod
├── internal/
│ ├── platform/
│ │ └── paths.go # ConfigDir(goos) — /etc/dumpstore or /usr/local/etc/dumpstore
│ ├── zfs/
│ │ ├── zfs.go # ListPools, ListDatasets, ListSnapshots, IOStats, PoolStatuses (direct CLI)
│ │ ├── acl.go # GetPosixACL, GetNFS4ACL — getfacl/nfs4_getfacl parsing
│ │ └── cronparse.go # Parse zfsutils-linux / zfstools cron entries for scrub schedules
│ ├── ansible/
│ │ ├── runner.go # Run(ctx, playbook, extraVars) → PlaybookOutput; ndjson output parsing; trace span per run
│ │ └── metrics.go # Prometheus counters/histograms for Ansible playbook runs
│ ├── otel/
│ │ └── otel.go # Env-gated OTEL SDK setup: trace/meter providers, log pipeline (no-op without OTLP env)
│ ├── logging/
│ │ ├── journal_exporter.go # OTEL log exporter reproducing the journald format (single producer)
│ │ ├── apphandler.go # Process-wide slog handler: otelslog bridge + level gate + req_id
│ │ └── middleware.go # RequestLogger: req_id correlation, span rename to METHOD /route
│ ├── auth/
│ │ ├── config.go # Load/save dumpstore.conf (username, password hash, TLS, trusted proxies)
│ │ ├── config_handlers.go # API handlers for auth config changes (username, password)
│ │ ├── login.go # Session-based login handler; argon2id password verify
│ │ ├── middleware.go # Auth middleware: session cookie + X-Remote-User trusted proxy
│ │ ├── ratelimit.go # Per-IP rate limiter for login endpoint
│ │ ├── session.go # In-memory session store
│ │ └── setpassword.go # --set-password CLI flag handler
│ ├── smb/
│ │ └── config.go # ParseSMBConfig, RenderSMBConfig — Go template for full smb.conf ownership
│ ├── api/
│ │ ├── handlers.go # Handler struct, RegisterRoutes, validation helpers, writeJSON/writeError
│ │ ├── zfs_handlers.go # ZFS: pools, datasets, snapshots, scrub, chown, auto-snapshot
│ │ ├── device_handlers.go # Block devices, zpool replace, device offline/online
│ │ ├── user_handlers.go # Users, groups, SSH key management
│ │ ├── acl_handlers.go # POSIX + NFSv4 ACL handlers
│ │ ├── smb_handlers.go # SMB: init, shares, users, homes, Time Machine
│ │ ├── iscsi_handlers.go # iSCSI target create/delete/list
│ │ ├── tls_handlers.go # TLS: status, self-signed gen, ACME issue/renew, load existing cert
│ │ ├── service_handlers.go # Service start/stop/restart/enable/disable (Samba, NFS, iSCSI)
│ │ ├── auth_handlers.go # Login, logout, session check
│ │ ├── metrics.go # GET /metrics — Prometheus exposition
│ │ ├── httpmetrics.go # HTTP middleware: request count + latency histograms
│ │ └── reqid.go # X-Request-ID middleware (read from proxy or generate)
│ ├── broker/
│ │ ├── broker.go # Thread-safe pub/sub (Subscribe/Publish/Unsubscribe)
│ │ └── poller.go # Background poller → publishes pool/dataset/snapshot/iostat/service changes
│ ├── schema/
│ │ └── schema.go # GET /api/schema — machine-readable API schema
│ ├── system/
│ │ ├── system.go # ListUsers, ListGroups, UIDMin, softwareVersions, getSysInfo
│ │ └── services.go # ListServices, ServiceStatus — systemd (Linux) and rc.d (FreeBSD)
│ ├── iscsi/
│ │ └── iscsi.go # ListTargets — targetcli saveconfig (Linux) / /etc/ctl.conf (FreeBSD)
│ ├── blockdev/
│ │ └── blockdev.go # List physical block devices (/sys/block, geom disk list) + vdev in-use matching
│ └── smart/
│ └── smart.go # ListDrives — smartctl per-disk health data
├── playbooks/
│ ├── inventory/localhost # ansible_connection=local, ansible_python_interpreter=auto_silent
│ │
│ ├── zfs_dataset_create.yml # Create filesystem or volume
│ ├── zfs_dataset_set.yml # Update dataset properties (set / inherit)
│ ├── zfs_dataset_destroy.yml # Destroy dataset or volume
│ ├── zfs_snapshot_create.yml # Create snapshot (optionally recursive)
│ ├── zfs_snapshot_destroy.yml # Destroy single snapshot
│ ├── zfs_snapshot_destroy_batch.yml # Destroy multiple snapshots in one run
│ ├── zfs_autosnap_set.yml # Set com.sun:auto-snapshot* properties per dataset
│ ├── zfs_scrub_start.yml # zpool scrub <pool>
│ ├── zfs_scrub_cancel.yml # zpool scrub -s <pool>
│ ├── zfs_scrub_periodic_enable.yml # Enable periodic scrub via FreeBSD periodic.conf
│ ├── zfs_scrub_periodic_disable.yml # Disable periodic scrub via FreeBSD periodic.conf
│ ├── zfs_scrub_zfsutils_enable.yml # Enable periodic scrub via zfsutils-linux cron
│ ├── zfs_scrub_zfsutils_disable.yml # Disable periodic scrub via zfsutils-linux cron
│ ├── zfs_disk_replace.yml # zpool replace <pool> <old> <new> (starts resilver)
│ ├── zfs_device_offline.yml # zpool offline <pool> <device>
│ ├── zfs_device_online.yml # zpool online <pool> <device>
│ │
│ ├── dataset_chown.yml # Set owner/group on dataset mountpoint
│ ├── acl_set_posix.yml # Add/modify POSIX ACL entry (setfacl -m)
│ ├── acl_remove_posix.yml # Remove POSIX ACL entry (setfacl -x)
│ ├── acl_set_nfs4.yml # Add NFSv4 ACL entry (nfs4_setfacl -a)
│ ├── acl_remove_nfs4.yml # Remove NFSv4 ACL entry (nfs4_setfacl -x)
│ │
│ ├── user_create.yml # Create local Unix user
│ ├── user_modify.yml # Modify user (shell, groups, password, home)
│ ├── user_delete.yml # Delete local user and home directory
│ ├── user_ssh_key_add.yml # Append entry to ~/.ssh/authorized_keys
│ ├── user_ssh_key_remove.yml # Remove entry from ~/.ssh/authorized_keys
│ ├── group_create.yml # Create local group
│ ├── group_modify.yml # Modify group (name, GID, members)
│ ├── group_delete.yml # Delete local group
│ │
│ ├── smb_init.yml # First-time Samba bootstrap: create dirs, render initial smb.conf, restart
│ ├── smb_apply.yml # Atomic full smb.conf render + restart (all SMB write ops call this)
│ ├── smb_usershare_set.yml # Create/update a usershare via net usershare
│ ├── smb_usershare_unset.yml # Remove a usershare
│ ├── smb_user_add.yml # Add user to smbpasswd / tdbsam
│ ├── smb_user_remove.yml # Remove user from smbpasswd / tdbsam
│ │
│ ├── iscsi_target_create.yml # Create iSCSI target (Linux / targetcli)
│ ├── iscsi_target_delete.yml # Remove iSCSI target (Linux / targetcli)
│ ├── iscsi_target_create_freebsd.yml # Create iSCSI target (FreeBSD / ctld)
│ ├── iscsi_target_delete_freebsd.yml # Remove iSCSI target (FreeBSD / ctld)
│ │
│ ├── tls_gencert.yml # Generate self-signed TLS certificate (openssl)
│ ├── tls_set_config.yml # Write TLS cert/key paths into dumpstore.conf
│ ├── tls_acme_issue.yml # Issue certificate via ACME / lego
│ ├── tls_acme_renew.yml # Renew ACME certificate
│ │
│ ├── auth_set_password.yml # Update argon2id password hash in dumpstore.conf
│ ├── auth_set_username.yml # Update username in dumpstore.conf
│ │
│ ├── service_control_linux.yml # systemctl start/stop/restart/enable/disable
│ └── service_control_freebsd.yml # service + sysrc enable/disable
├── images/ # Logo source files (SVG, all variants)
├── static/
│ ├── index.html # Single-page application shell + all dialogs
│ ├── app.js # Vanilla JS frontend — state, render fns, API calls
│ ├── style.css # Dark monospace theme; CSS variables in :root
│ └── images/ # Logos served by the HTTP file server
├── contrib/
│ ├── dumpstore.service # systemd unit file (Linux)
│ └── dumpstore.rc # rc.d script (FreeBSD)
├── install.sh # Standalone build-and-install script (Linux & FreeBSD)
└── Makefile # OS-aware build / install / uninstall
| Method | Path | Description |
|---|---|---|
| GET | /api/sysinfo |
Host and process info |
| GET | /api/version |
OpenZFS version string |
| GET | /api/pools |
List all pools with usage stats |
| GET | /api/poolstatus |
Detailed pool status with vdev tree |
| GET | /api/datasets |
List all datasets and volumes |
| GET | /api/dataset-props/{name} |
Editable properties for a dataset |
| GET | /api/snapshots |
List all snapshots |
| GET | /api/snapshots/diff |
Files changed between snapshots (zfs diff) |
| GET | /api/iostat |
Pool I/O statistics (1-second sample) |
| GET | /api/smart |
S.M.A.R.T. health per disk |
| GET | /api/devices |
List physical block devices (vdev candidates, in-use flag) |
| POST | /api/pools |
Create a pool (zpool create) |
| GET | /api/pools/importable |
List pools available for import |
| POST | /api/pools/import |
Import a pool (zpool import [-f]) |
| POST | /api/pools/{pool}/export |
Export a pool (zpool export) |
| POST | /api/pools/{pool}/vdevs |
Add a data vdev (zpool add) |
| POST | /api/pools/{pool}/cache |
Add an L2ARC cache device |
| POST | /api/pools/{pool}/log |
Add a SLOG log device (opt. mirrored) |
| POST | /api/pools/{pool}/spare |
Add a hot spare |
| DELETE | /api/pools/{pool}/devices/{device} |
Remove a cache/log/spare device (zpool remove) |
| POST | /api/pools/{pool}/replace |
Replace a pool device (zpool replace, starts resilver) |
| POST | /api/pools/{pool}/offline |
Take a pool device offline (zpool offline) |
| POST | /api/pools/{pool}/online |
Bring a pool device online (zpool online) |
| GET | /api/events |
Server-Sent Events stream (see below) |
| GET | /metrics |
Prometheus text exposition |
| POST | /api/datasets |
Create a dataset or volume |
| PATCH | /api/datasets/{name} |
Update dataset properties |
| POST | /api/rewrite/{name} |
Rewrite existing blocks (zfs rewrite, background job) |
| GET | /api/userspace/{name} |
Per-user/group space usage (?kind=user|group) |
| POST | /api/userquota/{name} |
Set or remove a per-user/group quota |
| DELETE | /api/datasets/{name} |
Destroy a dataset or volume |
| POST | /api/snapshots |
Create a snapshot |
| DELETE | /api/snapshots/{name} |
Destroy a snapshot |
| GET | /api/users |
List local users |
| POST | /api/users |
Create a local user |
| PUT | /api/users/{name} |
Edit user (shell, groups, password) |
| DELETE | /api/users/{name} |
Delete user and home directory |
| GET | /api/groups |
List local groups |
| POST | /api/groups |
Create a local group |
| PUT | /api/groups/{name} |
Edit group (name, GID, members) |
| DELETE | /api/groups/{name} |
Delete a local group |
| GET | /api/chown/{dataset} |
Get mountpoint owner and group |
| POST | /api/chown/{dataset} |
Set mountpoint owner and/or group |
| GET | /api/acl-status |
ACL presence map (dataset → bool) |
| GET | /api/acl/{dataset} |
Get ACL entries for a dataset |
| POST | /api/acl/{dataset} |
Add or modify an ACL entry |
| DELETE | /api/acl/{dataset} |
Remove an ACL entry |
| GET | /api/smb-shares |
List all active Samba usershares |
| POST | /api/smb-share/{dataset} |
Create or update a Samba usershare |
| DELETE | /api/smb-share/{dataset} |
Remove a Samba usershare |
| GET | /api/smb/status |
Samba init state, conf path, OS |
| POST | /api/smb/init |
Initialise Samba (create conf + dirs) |
| GET | /api/smb-users |
List users registered in smbpasswd |
| POST | /api/smb-users/{name} |
Add a user to smbpasswd |
| DELETE | /api/smb-users/{name} |
Remove a user from smbpasswd |
| GET | /api/smb/homes |
Get current SMB [homes] config |
| POST | /api/smb/homes |
Enable/update SMB [homes] section |
| DELETE | /api/smb/homes |
Disable/remove SMB [homes] section |
| GET | /api/smb/timemachine |
List all Time Machine shares |
| POST | /api/smb/timemachine |
Create/update a Time Machine share |
| DELETE | /api/smb/timemachine/{name} |
Remove a Time Machine share |
| GET | /api/iscsi-targets |
List all iSCSI targets |
| POST | /api/iscsi-targets |
Create an iSCSI target for a zvol |
| DELETE | /api/iscsi-targets |
Remove an iSCSI target |
| GET | /api/services |
List status of managed services (Samba, NFS, iSCSI) |
| POST | /api/services/{name}/{action} |
Control a service (start/stop/restart/enable/disable) |
{
"name": "tank/data",
"type": "filesystem",
"compression": "lz4",
"quota": "50G",
"mountpoint": "/mnt/data",
"recordsize": "128K",
"atime": "off",
"exec": "on",
"sync": "standard",
"dedup": "off",
"copies": "1",
"xattr": "sa"
}For volumes, use "type": "volume" and add "volsize": "10G". Optional: "volblocksize", "sparse": true.
Body is a JSON object with any subset of editable properties. An empty string value resets the property to inherited; a non-empty value sets it explicitly. Unknown properties are ignored.
{
"compression": "zstd",
"quota": "",
"readonly": "on"
}Editable properties: compression, quota, mountpoint, recordsize, atime, exec, sync, dedup, copies, xattr, readonly, acltype, sharenfs, sharesmb.
Append ?recursive=true to also destroy all child datasets and snapshots.
Pool roots (e.g. tank) cannot be deleted via this endpoint — use zpool destroy.
{
"dataset": "tank/data",
"snapname": "2024-01-15_backup",
"recursive": false
}Append ?recursive=true to also destroy clones.
Returns the ACL type and entries for the dataset's mountpoint.
{
"dataset": "tank/data",
"mountpoint": "/mnt/data",
"acl_type": "posix",
"entries": [
{ "tag": "user", "qualifier": "", "perms": "rwx", "default": false },
{ "tag": "user", "qualifier": "alice", "perms": "r-x", "default": false },
{ "tag": "group", "qualifier": "", "perms": "r-x", "default": false },
{ "tag": "mask", "qualifier": "", "perms": "rwx", "default": false },
{ "tag": "other", "qualifier": "", "perms": "---", "default": false }
]
}acl_type is one of "posix", "nfsv4", or "off". Entries are empty when acl_type is "off" or the dataset has no mountpoint.
For NFSv4 datasets each entry has the form:
{ "tag": "A", "flags": "fd", "qualifier": "OWNER@", "perms": "rwaDxtTnNcCoy" }Add or modify an ACL entry. The ace string format depends on the dataset's acltype:
- POSIX:
setfacl -mspec —"user:alice:rwx","group:storage:r-x","default:user:alice:rwx" - NFSv4: full ACE string —
"A::alice@localdomain:rwaDxtTnNcCoy","A:fd:GROUP@:rxtncoy"
{ "ace": "user:alice:rwx", "recursive": false }recursive (POSIX only) applies setfacl -R to all files inside the mountpoint. Returns Ansible task steps.
Remove an ACL entry. The entry query parameter is:
- POSIX: removal spec without perms —
user:alice,default:group:storage - NFSv4: full ACE string to match and remove
Append &recursive=true (POSIX only) to remove recursively.
List all iSCSI targets backed by ZFS volumes. Uses targetcli saveconfig on Linux or /etc/ctl.conf on FreeBSD. Returns an empty array if no backend is installed.
[
{
"iqn": "iqn.2024-03.io.dumpstore:tank-vms-win11",
"zvol_name": "tank/vms/win11",
"zvol_device": "/dev/zvol/tank/vms/win11",
"lun": 0,
"portals": ["0.0.0.0:3260"],
"auth_mode": "none",
"initiators": []
}
]Create an iSCSI target for a ZFS volume. Auto-selects the appropriate playbook based on platform (targetcli on Linux, ctld on FreeBSD). Returns 501 if no backend is detected.
{
"zvol": "tank/vms/win11",
"iqn": "iqn.2024-03.io.dumpstore:tank-vms-win11",
"portal_ip": "0.0.0.0",
"portal_port": "3260",
"auth_mode": "none",
"chap_user": "",
"chap_password": "",
"initiators": []
}zvol(required): ZFS volume name, must contain/iqn(required): RFC 3720 iSCSI Qualified Name (iqn.YYYY-MM.domain:name)portal_ip: listen IP, defaults to0.0.0.0portal_port: listen port, defaults to3260auth_mode:"none"or"chap"chap_user/chap_password: required whenauth_modeis"chap"initiators: array of allowed initiator IQNs; empty array = allow all
Remove an iSCSI target and its backstore. Both query parameters are required.
Server-Sent Events stream. The server pushes named events whenever data changes, eliminating the need for the client to poll.
Query parameter: topics — comma-separated list of topic names to subscribe to.
Available topics:
| Topic | Data | Source |
|---|---|---|
pool.query |
Same JSON as GET /api/pools |
Pushed every 10 s on change |
poolstatus |
Same JSON as GET /api/poolstatus |
Pushed every 10 s on change |
dataset.query |
Same JSON as GET /api/datasets |
Pushed every 10 s on change |
autosnapshot.query |
Same JSON as GET /api/auto-snapshot-schedules |
Pushed every 10 s on change |
snapshot.query |
Same JSON as GET /api/snapshots |
Pushed every 10 s on change |
iostat |
Same JSON as GET /api/iostat |
Pushed every 10 s always |
user.query |
Same JSON as GET /api/users |
Pushed on write op + every 10 s on change |
group.query |
Same JSON as GET /api/groups |
Pushed on write op + every 10 s on change |
service.query |
Same JSON as GET /api/services |
Pushed every 10 s on change |
replication.update |
Same JSON as GET /api/replication |
Pushed on task create/update/delete + after each scheduled run |
autosnap.status |
Same JSON as GET /api/auto-snapshot/status |
Pushed after takeover / release |
Each event follows the SSE wire format:
event: pool.query
data: [{"name":"tank","health":"ONLINE",...}]
event: iostat
data: [{"pool":"tank","read_ops":0,"write_ops":443,...}]
Example — watch pool health and I/O live:
curl -N 'http://localhost:8080/api/events?topics=pool.query,iostat'The browser UI uses EventSource to subscribe to all eight topics and falls back to 30 s REST polling automatically if the SSE connection is lost. User and group topics are also published immediately after any write operation so the UI reflects changes without waiting for the next poll cycle.
| Feature | Notes |
|---|---|
zfs rename; same-parent constraint; closes #21) |
|
zfs clone; closes #22) |
|
com.sun:auto-snapshot* ZFS properties; integrates with zfs-auto-snapshot / zfstools) |
|
| ZFS native encryption | Load/unload keys, show encryption status per dataset, support keyformat/keylocation |
targetcli/LIO on Linux, ctld on FreeBSD; IQN, portal, CHAP, initiator ACLs) |
|
| Pool import/export | Import available pools from attached devices; export pools safely |
| Snapshot diff | Show files changed between two snapshots (zfs diff) |
| Per-user quota tracking | Show space usage per user/group (zfs userspace / zfs groupspace) |
authorized_keys), move home directory |
|
[homes] section in smb.conf for per-user home directory shares[homes] section; configurable base path, browseable, read only, create/directory masks) |
|
vfs_fruit share configuration for macOS Time Machine backups over SMBvfs_fruit with catia and streams_xattr) |
|
| ZFS send/receive | Pool replication and off-site backup |
| Alerts | Configurable thresholds for pool health, disk temp, capacity |
zfsutils-linux, FreeBSD periodic.conf) |
|
sharenfs property; cross-platform) |
|
net usershare; Samba user management; setup playbook) |







