No description
  • TypeScript 97.6%
  • Svelte 2.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Julian Imhof 19b94cdf1e
feat: make rescan a host-owned task run
Core deprecated the fire-and-forget async route (core#21): long work is an
action the host owns, so it gets a persisted row, an actor, a lock and a
place in Activity instead of vanishing into a request that already returned.

/rescan re-probes host access and rebuilds every share from scratch — several
host processes plus a stat of each share root — so it qualifies. It becomes a
ctx.tasks.define action with two steps and a keyed progress row per share;
the route only starts the run and answers { runId }.

The action is `console: true` because it takes no arguments, which finally
gives the route a caller: nothing in the schema was bound to it, so until now
the only way to reach it was curl.

`expose` names entities and result only. Counts and share rows are structure;
host access failures name the probe strategy that failed, so they stay in the
withheld log.

No schedule and no onInterrupted retry: the 60s collector already refreshes
this state, so a schedule would duplicate it and re-running an interrupted
read would only add noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:31:14 +02:00
backend feat: make rescan a host-owned task run 2026-08-12 12:31:14 +02:00
frontend feat: dashboard overview card 2026-07-27 12:06:02 +02:00
CLAUDE.md feat: dashboard overview card 2026-07-27 12:06:02 +02:00
opsdeck.module.json feat: dashboard overview card 2026-07-27 12:06:02 +02:00
README.md feat: offer root and nobody a Samba password too 2026-07-24 17:01:28 +02:00

OpsDeck module: Network Shares

Create and manage SMB and NFS shares on the OpsDeck host.

This is a management module, not a monitor. The activity views exist to answer "is anyone using this right now" before you change something — they are not the point.

The idea

A share is a directory that people can reach. SMB and NFS are two ways to reach it, not two separate objects.

Tools that model them separately (cockpit-file-sharing among them) list the same directory in two tabs, configure it twice, and let the two definitions drift until the same file is writable one way and read-only the other. Here there is one share, with smb and nfs facets you toggle on it, and contradictions between them are reported instead of hidden.

Three more consequences of that stance:

  • Presets, not a wall of fields. "Private", "Household read-write", "Read-only on the LAN", "Media library", "Time Machine". Each expands to the eight parameters that actually implement it. Raw parameters remain editable.
  • Effective access. Share config is a filter on top of POSIX, never an override. read only = no over a 0755 root:root directory is the single most common share bug, and the resulting error names neither Samba nor the directory. The share page states the contradiction in plain language and offers the exact fix (chgrp + chmod 2775 + setgid).
  • Connect strings. Every share shows its \\host\name, smb://… and mount -t nfs … lines, ready to paste.
  • Samba passwords are not login passwords. Samba stores NT hashes in its own database (tdbsam), so a user whose SSH password works can still be refused by SMB with a bare "logon failure" — they simply have no Samba password. The Users page gives an existing unix account one, changes it, and enables, disables or removes it, and says plainly that the two are separate. root and nobody are offered alongside the real users — root for an administrative share, nobody because guest account maps to it — each labelled with what it means rather than quietly filtered out.

How it writes configuration

mechanism why
SMB net conf (registry) The host's smb.conf is [global] include = registry, so the registry is the config. Transactional, live pickup without a reload, no text file to parse or clobber.
NFS /etc/exports.d/opsdeck.exports + exportfs -ra One file we own and regenerate wholesale. /etc/exports is never touched, so hand-written exports keep working.

Reads (share lists, directory permissions, /etc/passwd) go through the host-root bind mount and need no privilege. Writes and daemon control run in the host's namespaces — see Requirements.

Requirements

The OpsDeck container needs the host-root bind mount (//host/root), writable — configuration is written on the host. Nothing else: the default CAP_SYS_CHROOT is enough, and no pid: host.

At startup the module probes four ways onto the host and keeps the first that works, testing whether the host's net/exportfs are actually reachable rather than whether the wrapper merely exited 0:

strategy how notes
chroot chroot /host/root … what the console module already uses; works under Docker's default profile
nsfile nsenter --mount=<hostProc>/1/ns/mnt … needs ptrace access to host PID 1, which default AppArmor denies
pidns nsenter -t 1 … only for containers sharing the host PID namespace
direct run it bare metal

If none work it degrades to read-only and says so on the overview page; mutations answer 503 with the reason.

Optional: pid: host

Two features identify a process by pid, which means nothing from a container with its own PID namespace — and no chroot trick changes that, because the obstacle is peer credentials, not the filesystem:

feature without pid: host why
Disconnect clients Can't find pid for destination 'smbd' smbcontrol signals smbd by pid
Start/stop Samba or NFS Failed to connect to bus: No data available as root systemctl uses /run/systemd/private, and PID 1 rejects a peer whose credentials it cannot resolve; the dbus system bus authorises the same way

Adding pid: host to the container fixes both, needs no extra capabilities, and is otherwise unnecessary — everything else, including all configuration, works without it. The module detects which case it is in and reports it as "Service control" on the overview page.

On the host: samba (any version with net conf), and nfs-kernel-server if you want NFS — the module offers to systemctl enable --now nfs-server when it finds it installed but stopped.

API

Method Path Purpose
GET /summary counts, service states, whether management is available
GET /shares every share, both protocols merged
GET /share/:id one share + effective-access report + live sessions
GET /presets, /principals, /users, /sessions pickers and activity
POST /shares create — preset form, flat form keys, or full {smb, nfs} facets
POST /share/:id update; a partial flat body edits, omitting a facet removes that protocol
POST /share/:id/delete remove the share; the directory is left alone
POST /share/:id/fix apply the suggested ownership/permission changes
POST /share/:id/disconnect close every open handle on the share
POST /preview dry run: the exact commands a change would execute
GET /services, /unix-users the daemons this module can start and stop; unix accounts eligible for a Samba password
POST /service/:id/enable, /service/:id/disable samba (smbd + nmbd) or nfs (nfs-server), persistently
POST /users, /user/:name/password give a user a Samba password, or change it
POST /user/:name/enable, /user/:name/disable, /user/:name/delete Samba account control

All mutations require the admin role.

Pages

Everything renders from the portable UI schema, so the web shell and the Android app show the same thing — including creating and editing:

page what
`` counts, service states, every share, "New share"
new create form: name, folder, access preset, people, network
share/:id facets, effective-access report, connect strings, live sessions, fix/disconnect/delete
share/:id/edit protocol facets as flat controls, prefilled from the share
clients SMB sessions, open files, NFS clients
users, user/:name Samba accounts, enable/disable

This needs core with the schema form block and navigating actions ({ label, page }) — both added alongside this module.

Development

cp -r . <core>/packages/modules/shares
cd <core>
OPSDECK_AUTH=disabled OPSDECK_MODULES=shares deno task start
curl localhost:8080/api/mod/shares/summary
curl localhost:8080/api/core/modules/shares/ui   # schema; invalid = module disabled

Remove the copy afterwards — this repo is the source of truth.