Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,21 @@ jobs:
chmod +x rootfiles
ROOTFILES_TEST_BINARY="$PWD/rootfiles" bash tests/scenarios/fleet.sh

tenant-scenario:
needs: unit
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: rootfiles-binary
path: .
- name: Tenant lifecycle on the runner's Docker and systemd
run: |
chmod +x rootfiles
ROOTFILES_TEST_BINARY="$PWD/rootfiles" bash tests/scenarios/tenant-lifecycle.sh

rocky:
needs: unit
runs-on: ubuntu-latest
Expand Down
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ make test # go test ./... -race
## Architecture

- `cmd/rootfiles/` — entry point
- `internal/cli/` — cobra commands (apply, backup, check, config, doctor, gpu, rollback, schedule, status, tunnel, update, user); `update` keeps `upgrade` as an alias. `fleet` is the operator-side controller; `runtime.go` holds the native-host root/lock preflight (native mutating commands are listed in `mutatingCommands`), audit logger and `newRunner`
- `internal/cli/` — cobra commands (apply, backup, check, config, doctor, gpu, rollback, schedule, status, tenant, tunnel, update, user); `update` keeps `upgrade` as an alias. `fleet` is the operator-side controller; `runtime.go` holds the native-host root/lock preflight (native mutating commands are listed in `mutatingCommands`), audit logger and `newRunner`
- `internal/config/` — config structs, YAML profile loader (profiles merged as YAML trees, strict keys), `Validate()`, system detector
- `internal/module/` — Module interface + 13 implementations (locale, system, packages, users, ssh, security, docker, nvidia, gpu, cloudflared, storage, network, monitoring), plus `doctor.go`
- `internal/module/` — Module interface + 14 implementations (locale, system, packages, users, ssh, security, docker, nvidia, gpu, cloudflared, storage, network, monitoring, tenant), plus `doctor.go`
- `internal/tenant/` — `rootfiles tenant` specs, containers, volumes, passwords, import and audit; it must not import `internal/module` (module imports it). Design: `docs/specs/2026-10-20-tenant-containers-design.md`
- `internal/exec/` — shell runner (dry-run aware; `Runner.Backup` preserves files before WriteFile/Remove/Rename/Symlink), APT and RPM/DNF package-manager backends
- `internal/state/` — `/var/lib/rootfiles/state.json` + history, global flock
- `internal/ui/` — interactive prompts (Charm huh) + shared output styling: `styles.go` (lipgloss palette), `markers.go` (✓ ✗ → ⚠), `format.go` (WriteHeader/Section/KV/Hint/Bullet). lipgloss auto-detects TTY and honours `NO_COLOR`; all status-style reports must go through these helpers.
Expand All @@ -35,7 +36,7 @@ type Module interface {
- `Check()` reports pending changes without side effects; `CheckResult.Satisfied` gates whether `Apply()` runs.
- `Apply()` performs idempotent changes; returns `ApplyResult.Changed` so callers can summarise diffs.

`NewRegistry()` in `internal/module/module.go` wires all 13 implementations. `defaultOrder` in the same file defines execution sequence. These two lists MUST stay in sync — `TestRegistryDefaultOrderSync` in `module_test.go` enforces this.
`NewRegistry()` in `internal/module/module.go` wires all 14 implementations. `defaultOrder` in the same file defines execution sequence. These two lists MUST stay in sync — `TestRegistryDefaultOrderSync` in `module_test.go` enforces this.

## Conventions

Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ Managed hosts must run Linux; macOS is not a supported host OS. The macOS build

`rootfiles` is the root half of a two-tool server setup. It prepares a machine with everything that needs root and stops at the user account; [dotfiles](https://github.com/STAIxBWLB/dotfiles) (the `dot` binary) then sets up each user's own environment inside that account, on servers as well as on macOS and Linux workstations.

- **One host, declaratively.** `rootfiles apply` converges a host to a profile or site config through 13 modules. `check` reports drift without changing anything, `doctor` checks live health (SSH access, disks, reboot, GPUs), and every file a run changes is backed up for `rollback`.
- **One host, declaratively.** `rootfiles apply` converges a host to a profile or site config through 14 modules. `check` reports drift without changing anything, `doctor` checks live health (SSH access, disks, reboot, GPUs), and every file a run changes is backed up for `rollback`.
- **A fleet over SSH.** `rootfiles fleet` runs `status`, `check` and `doctor` in parallel and `apply`, `update`, `bootstrap` and `schedule` serially from an operator's Mac or Linux machine. Config fingerprints show which configuration each host applied, and `fleet update` flags hosts that still need `apply`.
- **Tenant containers.** `rootfiles tenant` gives outside groups a long-lived SSH container each, with a size-capped volume, accounts and host keys that survive image changes, and GPUs from the shared allocation ledger ([guide](docs/tenant.md)).
- **Monitoring.** Node and DCGM GPU exporters on each host, an optional Prometheus, Alertmanager and Grafana hub, and scheduled `check`/`doctor` reports exported as metrics.

```
Expand Down Expand Up @@ -146,6 +147,7 @@ All modules are idempotent and support `--dry-run`.
| `storage` | RAID/NVMe directory setup, symlinks |
| `network` | UFW firewall, port rules (SSH port always allowed) |
| `monitoring` | Opt-in Prometheus node exporter and DCGM GPU exporter, plus an optional Prometheus/Alertmanager/Grafana hub that reloads on rule changes |
| `tenant` | Opt-in host prep for `rootfiles tenant`: GPU device guard timer, daily tenant password sync, fstrim, operator sudoers rule, tenant volume mount units |

Modules run in this order; `users` precedes `ssh` so declared operators exist before password auth is turned off.

Expand Down Expand Up @@ -488,6 +490,18 @@ MIG mode and instances (read-only; repartition with `nvidia-smi mig`):
rootfiles gpu mig status
```

### Tenant containers

Long-lived SSH containers for outside groups, one per tenant, with a size-capped volume and GPUs from the allocation ledger. Full guide: [docs/tenant.md](docs/tenant.md).

```bash
sudo rootfiles apply --module tenant # once per host (modules.tenant.enabled)
sudo rootfiles tenant create est --image IMAGE --ssh-port 3889 --gpus 2,3 --size 2T --account est:1000 --sudo
sudo rootfiles tenant image est IMAGE2 # recreate; data, passwords, host key and GPUs kept
sudo rootfiles tenant volume resize est 3T # grows online
sudo rootfiles tenant status
```

### Cloudflare tunnel + VLAN

```bash
Expand Down Expand Up @@ -562,10 +576,11 @@ Interactive blueprint: [docs/architecture/rootfiles-v2-rendered.html](docs/archi
cmd/rootfiles/ Entry point
internal/
cli/ Cobra commands (apply, backup, check, config, doctor, gpu, rollback,
schedule, status, tunnel, update, user) + root/lock preflight
schedule, status, tenant, tunnel, update, user) + root/lock preflight
config/ YAML profiles (tree-merged extends), validation, system detector
profiles/ Embedded profile YAMLs (go:embed)
module/ 13 modules implementing the Module interface, doctor checks
module/ 14 modules implementing the Module interface, doctor checks
tenant/ Tenant specs, containers, volumes, passwords, import, audit
exec/ Shell runner (dry-run aware, per-run file backups), APT wrapper
state/ Applied-state record, history, global lock
ui/ Interactive prompts (Charm huh) + shared output styling
Expand All @@ -590,6 +605,7 @@ Every push and pull request runs:
| `module` | 2 OS × 9 modules + GPU on DGX mock (isolated module execution) |
| `scenario` | 14 E2E flows: dry-run-all-profiles, user backup/restore, user rehome, user list names, user lifecycle, user quota, declarative accounts, tunnel setup/teardown, OS reinstall recovery, system backup, GPU allocation, status, SSH lockout guard, state/rollback/lock |
| `fleet-scenario` | Fleet controller against two SSH containers (`tests/scenarios/fleet.sh`): status, serial stop rule, `--skip-unreachable` |
| `tenant-scenario` | Tenant lifecycle on the runner's Docker and systemd (`tests/scenarios/tenant-lifecycle.sh`): temporary password login, recreate, quota and growth, operator delegation, dry run, import |
| `rocky` | Rocky 8.9 / 8.10 / 9 images: native core profile, SSH and installer (`tests/run-rocky.sh`) |
| `monitoring-hub` | Generated monitoring hub acceptance (`tests/scenarios/monitoring-hub.sh`) |
| `release` | GoReleaser on every `v*` tag push, after the tagged commit's main-push Test run succeeded (Test does not run for tags); publishes `docs/releases/<tag>.md` as the release body and fails when it is missing or fails `scripts/check-release-notes.sh` (format: `AGENTS.md`, Release notes) |
Expand Down
13 changes: 13 additions & 0 deletions docs/releases/v0.16.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
GPU hosts can now run long-lived SSH containers for outside groups with `rootfiles tenant`, each with a size-capped volume, accounts that survive image changes and GPUs from the shared allocation ledger.

### New
- `rootfiles tenant create` starts a tenant container with its own SSH port, GPUs on the NVIDIA runtime (kept after a `systemctl daemon-reload`) and a volume for `/data`, `/home` and `/tmp` capped at `--size`. Each account gets a temporary password that must be changed at first login; it is printed once and never logged. (#79)
- `rootfiles tenant image`, `gpu` and `port` recreate the container and keep the volume, the accounts' passwords, the SSH host key and GPU access. A failed check of the new container rolls back to the old one, which is otherwise kept as `tenant-<name>-prev`. (#79)
- `rootfiles tenant volume resize` grows a volume while the container runs; `--offline` shrinks an ext4 volume. `rootfiles tenant status` shows volume use, spec drift and whether the volume sizes exceed the free space. (#79)
- `rootfiles tenant user add`, `passwd`/`reset`, `restart`, `logs`, `apply`, `list` and `sync` manage accounts and containers; every change is recorded in `/var/log/rootfiles-tenant.jsonl`. (#79)
- Host accounts in the operator group can run `sudo rootfiles tenant ops <tenant> status|logs|gpu-heal|restart|reset` for the tenants that list them, without other sudo rights. (#79)
- `rootfiles tenant import` moves a hand-made container under rootfiles: it copies its accounts, passwords, host keys and the data you name into a new volume, keeps the old container stopped, and refuses before any change when the data does not fit. (#79)
- The opt-in `tenant` module (`modules.tenant.enabled`) installs a GPU device guard that restores GPU access of `--gpus` containers after a `systemctl daemon-reload`, a daily password sync, `fstrim.timer`, the operator sudoers rule and the tenant volume mount units. See `docs/tenant.md`. (#79)
- `rootfiles gpu assign` now also refuses GPUs that a tenant holds. (#79)

**Full changelog**: https://github.com/STAIxBWLB/rootfiles/compare/v0.15.1...v0.16.0
2 changes: 1 addition & 1 deletion docs/rocky-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The Rocky profile targets Rocky Linux 8.9, 8.10, and Rocky Linux 9. Other Rocky
| `users` | Supported | Administrator membership uses `wheel`; existing homes, keys, passwords, and other groups are preserved. |
| `ssh` | Supported | If the main sshd config lacks `/etc/ssh/sshd_config.d/*.conf`, Apply backs it up and adds a global Include at the top while preserving its mode. It validates `sshd -t` and effective settings before reload, restoring the main config and managed drop-in on validation failure. SSH port changes require a compatible SELinux `ssh_port_t` label and unrestricted TCP allowances for both address families in UFW, or an allowance in every active firewalld zone. Source-, family-, zone-limited, or complex deny/rich-rule policies need operator preparation and console verification; rootfiles never opens or broadens firewall policy. |
| `security` | Supported with limits | `dnf-automatic-install.timer` applies security updates only from Rocky `baseos` and `appstream`, excludes NVIDIA/CUDA packages, and sets reboot to `never`. Check reads and parses the DNF cache without refreshing it; expired but parseable metadata does not make a correct policy appear drifted, while absent or malformed required metadata reports pending refresh. Apply refreshes metadata and strictly verifies fresh `updateinfo` for both required repositories before writing policy. This Check behavior does not promise current patches. Other enabled repositories without usable `updateinfo` produce an explicit coverage warning on Apply; they are not covered by the security-only guarantee. `chronyd` provides time synchronization. `fail2ban` is rejected unless a later release verifies an explicit EPEL configuration. |
| `docker`, `nvidia`, `gpu`, `cloudflared`, `storage`, `network`, `monitoring` | Unsupported | Rocky capability validation rejects these enabled modules before module execution. No firewall, container, GPU, tunnel, storage, or monitoring vendor stack is changed. |
| `docker`, `nvidia`, `gpu`, `cloudflared`, `storage`, `network`, `monitoring`, `tenant` | Unsupported | Rocky capability validation rejects these enabled modules before module execution, and `rootfiles tenant` refuses to run. No firewall, container, GPU, tunnel, storage, or monitoring vendor stack is changed. |

Use `--profile rocky` as the starting point for a Rocky host. Review package names and SSH settings for the target before applying. A reduced configuration does not enable unsupported modules; requesting one produces an explicit capability error.

Expand Down
Loading
Loading