Documentation
¶
Overview ¶
Package snapshot orchestrates btrfs snapshot tooling (btrbk, systemd) through a single declarative TOML config. bentoolkit is an orchestrator, not a btrfs implementation: it renders native config for mature tools and coordinates them; no destructive btrfs logic is written in Go.
This file holds the config model and path resolution. The four orchestration interfaces (Engine, Shipper, Notifier, Scheduler) live in their own files, the concrete drivers in engine_btrbk.go / ship_ssh.go / systemd.go, and the run pipeline in manager.go.
Index ¶
- Constants
- Variables
- func Apply(ctx context.Context, cfg *Config, configPath string, run Runner) error
- func ArchiveObjectLeaf(id string) string
- func ArchiveObjectName(subvolume, id string) string
- func ArchivePrefix(subvolume string) string
- func BtrbkConfPath(snapshotConfPath string) string
- func ConfigPaths() ([]string, error)
- func DefaultConfigPath() (string, error)
- func FindConfigPath() (string, error)
- func InstallEmergeHook() error
- func LastRunPath() string
- func PlanApply(cfg *Config, configPath string) []string
- func PlanPrune(cfg *Config, shipScope string) []string
- func PlanRun(cfg *Config) []string
- func ResolveRestoreSubvolume(cfg *Config, flag string) (string, error)
- func Restore(ctx context.Context, id, target string, opts RestoreOptions) error
- func RestoreChainFor(cfg *Config, ship ShipConfig, id, subvolume string) []chainLink
- func Rollback(ctx context.Context, cfg *Config, id string, opts RollbackOptions) error
- func TimerNextRun(ctx context.Context, run Runner) string
- func TimerState(ctx context.Context, run Runner) string
- func UninstallEmergeHook() error
- func WriteBtrbkConf(cfg *Config, configPath string) error
- func WriteEngineConfig(ctx context.Context, cfg *Config, configPath string, run Runner) error
- type Config
- type EmailConfig
- type Engine
- type EngineConfig
- type HealthchecksConfig
- type Manager
- func (m *Manager) List(ctx context.Context) (map[string][]Snapshot, error)
- func (m *Manager) ListRemote(ctx context.Context) []RemoteGroup
- func (m *Manager) Prune(ctx context.Context, shipScope string) (RunResult, error)
- func (m *Manager) Run(ctx context.Context) (RunResult, error)
- func (m *Manager) Subvolumes() []string
- type ManagerOption
- type Notifier
- type NotifyConfig
- type NtfyConfig
- type PipeStage
- type Piper
- type RemoteGroup
- type RestoreOptions
- type Retention
- type RollbackOptions
- type RunResult
- type Runner
- type SMTPConfig
- type ScheduleConfig
- type Scheduler
- type ShipConfig
- type ShipReport
- type Shipper
- type Snapshot
- type StageResult
- type WebhookConfig
Constants ¶
const ( StageCreate = "create" StagePrune = "prune" StageShip = "ship" StageGFS = "gfs" )
Stage names recorded in a RunResult, in pipeline order. StageGFS is the on-demand remote GFS sweep of `snapshot prune` — distinct from StagePrune (the engine-local prune) so a RunResult tells the two apart.
const ( StatusOK = "ok" StatusFailed = "failed" )
Stage outcome statuses.
const DefaultBtrbkConfPath = "/etc/bentoo/btrbk.conf"
DefaultBtrbkConfPath is where `apply` writes the rendered btrbk.conf and where the engine points btrbk via `-c`.
Variables ¶
var EmergeHookRoot = "/"
EmergeHookRoot is the filesystem root under which the Portage emerge-hook paths are WRITTEN (hook script + bashrc). It is "/" in production; tests point it at a temp dir — mirroring the exported StateDir seam in result.go.
Note the write/runtime distinction: EmergeHookRoot only prefixes where files are written. The managed bashrc block always sources the hook script's absolute RUNTIME path (/etc/portage/bashrc.d/...), never the prefixed one, because that is where Portage will find it on the running system.
var ErrBrokenChain = errors.New("archive restore chain is broken")
ErrBrokenChain is returned when an archive restore's delta chain is not a contiguous full→…→target sequence — an empty chain, a first link that is not a full, or a gap where a delta's parent is missing. It is the backstop: a broken chain is refused BEFORE any `btrfs receive` runs, so a restore can never apply a delta whose base is absent.
var ErrBrokenHookBlock = errors.New("bentoo snapshot hook block has no end marker")
ErrBrokenHookBlock marks a bashrc whose managed-block begin marker has no end marker after it. Stripping such a block would drop every line to the end of the file, so install and uninstall refuse it instead.
ErrDriverUnavailable is returned when an active driver's binary is absent from PATH. The message names the Portage package to install.
var ErrEngineFailed = errors.New("snapshot engine command failed")
ErrEngineFailed wraps a non-zero exit from the underlying engine binary (e.g. `btrbk run`). Stage errors in a RunResult are built by joining this with the captured stderr.
var ErrInvalidConfigValue = errors.New("invalid snapshot config value")
ErrInvalidConfigValue is returned by Validate when a value that reaches btrbk.conf or a systemd unit carries a control character. A newline there would inject a directive of the file's own grammar.
var ErrInvalidDriver = errors.New("invalid snapshot driver")
ErrInvalidDriver is returned by the factories (and by Config.Validate) when an engine.driver, ship.type, or schedule.backend names an unknown driver. It is the single shared sentinel for "unknown driver string" across the package, mirroring provider.ErrInvalidProvider.
var ErrInvalidShipTarget = errors.New("invalid ssh ship target")
ErrInvalidShipTarget is returned when an ssh ship entry has a missing or malformed target (expected user@host:/path).
var ErrRestoreDeclined = errors.New("restore declined by operator")
ErrRestoreDeclined is returned when the operator does not approve a destructive restore at the confirm prompt. When this is returned, NOTHING has been applied — the gate fires before any subprocess.
var ErrRollbackDeclined = errors.New("rollback declined by operator")
ErrRollbackDeclined is returned when the operator does not approve a destructive rollback at the confirm prompt. When this is returned, NOTHING has run — the gate fires before any subprocess. Mirrors ErrRestoreDeclined.
var ErrRollbackUnsupported = errors.New("rollback requires the snapper engine")
ErrRollbackUnsupported is returned when the active engine is not snapper: `snapper rollback` has no btrbk equivalent, so rollback is refused outright with a clear message rather than approximated.
var ErrSchedulerFailed = errors.New("snapshot scheduler command failed")
ErrSchedulerFailed wraps a non-zero exit from systemctl.
var ErrShipEngineMismatch = errors.New("ship type not supported by engine")
ErrShipEngineMismatch is returned by Validate when a ship type cannot work with the configured engine: ssh shipping is btrbk's own transfer, so snapper cannot perform it.
var ErrSnapshotUnidentified = errors.New("snapshot has no path or id")
ErrSnapshotUnidentified is returned by the ships that address a snapshot by its Path and ID (archive, restic) when the engine could not resolve them. The snapshot exists; the ship cannot find it.
var StateDir = func() string { return "/var/lib/bentoo/snapshot" }
StateDir is the directory holding persisted snapshot state. It is a var so tests can redirect it away from the system path.
Functions ¶
func Apply ¶
Apply materializes the native config and scheduler for cfg: it renders+writes the engine's native config (driver-aware) and, when a systemd schedule is configured, installs and enables the timer. run is the injectable subprocess seam.
func ArchiveObjectLeaf ¶
ArchiveObjectLeaf is the object name RELATIVE to its prefix directory: "<id>.zst". This is exactly what `rclone lsjson <remote>/<prefix>` reports in each entry's Name — relative to the LISTED path, carrying no prefix — so a scoped listing of "-home" yields "snap1.zst", not "-home/snap1.zst".
Comparing a listing entry against a FULL key (ArchiveObjectName) therefore matches nothing, and it does so SILENTLY: no error, no empty result, just a comparison that is never true. Where that comparison is a guard, "matches nothing" means "the guard protects nothing". Compare listing entries with this function; use ArchiveObjectName only for keys relative to the remote root.
func ArchiveObjectName ¶
ArchiveObjectName derives the deterministic FULL remote object key — relative to the remote ROOT — for a snapshot of subvolume with the given id: "<ArchivePrefix(subvolume)>/<ArchiveObjectLeaf(id)>", e.g. subvolume "/home" with id "snap1" → "-home/snap1.zst". It is the EXPORTED mirror of the unexported archiveObjectName(Snapshot) used on the ship side, so the restore path can build the object key from a subvolume + id pair without holding a full Snapshot value. Keeping both on the same convention guarantees the restore reads back exactly the key the archive shipper wrote.
This is a full key, NOT a listing entry: see ArchiveObjectLeaf for what `rclone lsjson` reports under a scoped path.
func ArchivePrefix ¶
ArchivePrefix is the remote sub-path that holds one subvolume's objects: the subvolume path put through the same sanitize rule the parent store already uses for its filenames, with NO special case — including for the root subvolume "/", whose prefix is therefore the single directory "-".
The property that makes this prefix unambiguous is that sanitize maps every byte outside [A-Za-z0-9._-] to '-' and so can NEVER emit '/'. The '/' that ArchiveObjectName appends is therefore the only one in the key: a prefix can never bleed into the leaf, and two nested subvolumes can never produce the same key. The old flat scheme joined prefix and id with '-', a byte sanitize CAN emit, so subvolume "/home" with id "otaku-42" and subvolume "/home/otaku" with id "42" both rendered as "-home-otaku-42.zst".
func BtrbkConfPath ¶
BtrbkConfPath returns where the rendered btrbk.conf lives for a given snapshot.toml path — the sibling file btrbk.conf in the same directory. An empty snapshotConfPath falls back to DefaultBtrbkConfPath.
func ConfigPaths ¶
func DefaultConfigPath ¶
DefaultConfigPath returns the highest-priority config path (system scope). It is the write target for `apply`-generated config.
func FindConfigPath ¶
FindConfigPath returns the first existing config path, or the default path when none exists yet (so callers have a stable location to create one).
func InstallEmergeHook ¶
func InstallEmergeHook() error
InstallEmergeHook installs the opt-in Portage emerge hook: it writes the snapper pre/post hook script to /etc/portage/bashrc.d and ensures /etc/portage/bashrc sources it via the managed block. Idempotent: re-running rewrites the script and replaces the existing block in place — user bashrc content outside the block is preserved byte-for-byte and the block is never duplicated.
It is called exclusively by the explicit `bentoo snapshot hook --install` verb — never by apply.
func LastRunPath ¶
func LastRunPath() string
LastRunPath is the canonical path of the most recent RunResult.
func PlanApply ¶
PlanApply returns the actions `snapshot apply` would perform for cfg as human-readable "would ..." lines: the engine config work it would do (driver-aware, mirroring WriteEngineConfig — a file for btrbk, snapper commands for snapper) and — when a systemd schedule is configured — the unit files it would install plus the systemctl invocations (mirroring systemdScheduler.Apply). Callers validate cfg first; an unknown engine driver contributes no engine line.
func PlanPrune ¶
PlanPrune returns the actions `snapshot prune` would perform for cfg as human-readable "would ..." lines: per subvolume the engine-native prune (driver-aware invocation summary), then one remote GFS line per in-scope archive ship with the retention it would enforce — Manager.Prune's order. A non-empty shipScope mirrors the real --ship scoping: the engine line(s) are omitted and only the named ship's remote is planned. Callers reject an unknown shipScope BEFORE planning (the cmd layer exits 1), so an unmatched scope here simply yields no lines.
func PlanRun ¶
PlanRun returns the pipeline `snapshot run` would execute for cfg as human-readable "would ..." lines: per subvolume the engine create+prune, then one ship line per [[ship]] target, in manager.go's Run order.
func ResolveRestoreSubvolume ¶
ResolveRestoreSubvolume picks the subvolume a restore reads from, given the engine's configured list and the value of the --subvolume flag ("" when the operator did not pass one):
- a non-empty flag must name a configured subvolume, else it fails naming the value passed and listing the configured spellings;
- an empty flag with exactly ONE configured subvolume yields that one, so the single-subvolume deployment keeps working with no new flag;
- an empty flag with TWO OR MORE is ambiguous and fails, naming every configured subvolume so the operator can retry without opening the config.
The rule lives in the package, not the cobra handler, because restore is DESTRUCTIVE: as a pure function over (config, flag) a unit test pins it branch by branch. Callers resolve BEFORE calling Restore, so an ambiguous request fails ahead of any subprocess. Matching is exact string equality: the configured spelling is the key ArchivePrefix sanitizes, so normalising the flag (trimming a trailing '/', say) would send the restore to the wrong objects.
func Restore ¶
func Restore(ctx context.Context, id, target string, opts RestoreOptions) error
Restore restores snapshot id into target, dispatching by opts.Driver. Because every restore is destructive, it first enforces the confirm gate: unless opts.Yes is set, it asks opts.Confirm (or defaultConfirmFunc) to approve, and returns ErrRestoreDeclined — running NOTHING — if the operator declines. Only then does it dispatch: "archive" replays the validated delta chain, "restic" runs a (optionally granular) `restic restore`; an unknown driver is rejected with ErrInvalidDriver.
func RestoreChainFor ¶
func RestoreChainFor(cfg *Config, ship ShipConfig, id, subvolume string) []chainLink
RestoreChainFor builds the archive object chain the restore CLI replays for id. It is the EXPORTED seam that keeps chain construction INSIDE this package: chainLink is unexported and cannot be built from package main, so the CLI assigns the result straight into RestoreOptions.Chain. A restic ship needs no chain and gets nil.
subvolume is the ALREADY-RESOLVED subvolume (the caller runs ResolveRestoreSubvolume first); this function makes no choice of its own, since deriving it from the FIRST configured subvolume sent multi-subvolume restores to the wrong prefix. For an archive ship it returns a SINGLE full link (ParentID "") keyed ArchiveObjectName(subvolume, id), the key the archive shipper wrote. cfg is unread but kept: a real chain needs the ship list and retention, and churning an exported signature twice costs more than that.
TODO(incremental-chain): reconstruct the full→…→target delta sequence for an incremental id by listing the remote. Until then a delta-only id fails (correctly) at `btrfs receive` because its base is absent.
func Rollback ¶
Rollback rolls the system back to snapshot id via `snapper rollback`. The ORDER is the contract:
- ENGINE GUARD: a non-snapper cfg.Engine.Driver is refused with ErrRollbackUnsupported BEFORE the confirm gate — the operator is never prompted to approve an action that cannot run.
- CONFIRM GATE: unless opts.Yes, the operator must approve; a decline returns ErrRollbackDeclined and NO subprocess runs.
- EXECUTE: `snapper -c root rollback <id>` through opts.Run. A system rollback always targets the canonical "root" snapper config (snapperConfigName("/")) — rolling back is only meaningful for the root filesystem.
func TimerNextRun ¶
TimerNextRun reports the timer's next scheduled run via `systemctl list-timers bentoo-snapshot.timer --no-pager`. Like TimerState it is best-effort: it returns the trimmed data line naming the timer unit, or "" when no such line is available (callers print "unknown").
func TimerState ¶
TimerState reports the systemd timer's enablement via `systemctl is-enabled bentoo-snapshot.timer`. It is best-effort: a non-zero exit (e.g. the timer is disabled or absent) is mapped to the trimmed output rather than a hard error, so `status` always has something to print.
func UninstallEmergeHook ¶
func UninstallEmergeHook() error
UninstallEmergeHook removes the Portage emerge hook: it deletes the hook script and strips the managed block from /etc/portage/bashrc, leaving user content intact. A bashrc left empty (or whitespace-only) is removed. Absent files are a clean no-op so uninstall succeeds on a never-installed system.
func WriteBtrbkConf ¶
WriteBtrbkConf renders the btrbk.conf for cfg and writes it next to the snapshot.toml at configPath. Called by `apply` and by `run` so the run is self-contained even if `apply` was never executed.
func WriteEngineConfig ¶
WriteEngineConfig materializes the native config for the configured engine driver: "btrbk" renders btrbk.conf next to the snapshot.toml at configPath; "snapper" provisions the per-subvolume configs through snapper's own create-config/set-config (configPath is unused there — snapper's config location is fixed). An unknown driver fails with ErrInvalidDriver.
It takes a context and a Runner because the snapper driver reaches snapper to do the work rather than writing /etc itself — a file write is not observed by a running snapperd. The btrbk driver uses neither.
The snapper driver's warnings go to the logger ctx carries (logging.FromContext); a ctx without one discards them.
Types ¶
type Config ¶
type Config struct {
Engine EngineConfig `toml:"engine"`
Ship []ShipConfig `toml:"ship,omitempty"`
Notify NotifyConfig `toml:"notify,omitempty"`
Schedule ScheduleConfig `toml:"schedule,omitempty"`
}
Config is the parsed snapshot.toml. The engine produces snapshots, each ship replicates them, notify reports the outcome, and schedule installs the systemd timer that drives `bentoo snapshot run`.
func LoadFrom ¶
LoadFrom reads and parses snapshot.toml from a specific path. It does not validate; call Validate after loading.
Parsing stays lenient: an unknown key is ignored exactly as before. The decode runs through a Decoder purely to obtain the MetaData that drives the ONE targeted migration diagnostic below — toml.Unmarshal is itself just this call with the MetaData discarded, so behavior, errors, and leniency are unchanged. This is deliberately not a general strict-decode pass: only the single removed key is reported, so forward-compatible additions to snapshot.toml stay silent.
LoadFrom is LoadFromWith(path, nil): the migration warning goes to stderr as a bare line.
func LoadFromWith ¶
LoadFromWith is LoadFrom reporting the migration warning on log, the invocation's logger. A nil log keeps LoadFrom's bare stderr line.
func (*Config) Validate ¶
Validate checks the config before any side effect. It fails hard with ErrInvalidDriver on an unknown engine.driver, ship.type, or schedule.backend; warns-but-continues on non-fatal issues (empty subvolumes); and verifies each active driver's binary is on PATH via detect.
Order matters: every enum is checked first, so an unknown driver string is reported before — and independently of — any missing-binary detection, and both happen before the command writes any file.
Validate logs nothing: it is ValidateWith(nil), and a nil logger discards.
type EmailConfig ¶
type EmailConfig struct {
To []string `toml:"to,omitempty"`
From string `toml:"from,omitempty"`
SMTP SMTPConfig `toml:"smtp,omitempty"`
}
EmailConfig configures the email driver. A non-empty To activates the driver; From is the sender header. With SMTP.Host unset the message is piped to the local sendmail binary; setting SMTP.Host switches the transport to direct SMTP via stdlib net/smtp.
type Engine ¶
type Engine interface {
Name() string
Create(ctx context.Context, subvolume string) (Snapshot, error)
Prune(ctx context.Context, subvolume string, policy Retention) ([]Snapshot, error)
List(ctx context.Context, subvolume string) ([]Snapshot, error)
}
Engine is the snapshot engine contract. Drivers (btrbk, snapper) create, prune, and list snapshots for a subvolume. Retention is delegated to the native tool — Prune passes the policy through rather than computing GFS in Go.
type EngineConfig ¶
type EngineConfig struct {
Driver string `toml:"driver"`
Subvolumes []string `toml:"subvolumes,omitempty"`
SnapshotDir string `toml:"snapshot_dir,omitempty"`
Retention Retention `toml:"retention,omitempty"`
}
EngineConfig selects and configures the snapshot engine driver. Subvolumes are the btrfs subvolumes to snapshot; SnapshotDir is where the engine keeps them.
type HealthchecksConfig ¶
type HealthchecksConfig struct {
PingURL string `toml:"ping_url,omitempty"`
Start bool `toml:"start,omitempty"`
}
HealthchecksConfig configures the healthchecks.io driver. PingURL is the base check URL (success → base, failure → /fail); Start optionally pings /start before the run.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager wires the configured drivers and runs the snapshot pipeline.
func NewManager ¶
NewManager builds a Manager from cfg using the factories. configPath is the snapshot.toml path; it locates the sibling btrbk.conf the engine drives. run is the injectable subprocess seam (nil → production execRunner); ssh targets are folded into the engine's btrbk.conf so btrbk performs send/receive during Create.
func (*Manager) List ¶
List returns the local snapshots per configured subvolume. A subvolume that errors aborts the listing with that error.
func (*Manager) ListRemote ¶
func (m *Manager) ListRemote(ctx context.Context) []RemoteGroup
ListRemote collects the remote snapshot listings of every capable source in deterministic order — the engine's target backups first, then each ship in config order. A failing source is recorded in its group's Err and does NOT abort the others, keeping `list` lenient and read-only.
func (*Manager) Prune ¶
Prune applies the [engine.retention] policy on demand: the engine-native prune per subvolume (btrbk clean / snapper cleanup timeline), then the remote GFS sweep per archive ship. A non-empty shipScope narrows the prune to the named destination ONLY: the engine-local prune is skipped entirely and just that ship's remote is pruned; an unknown scope returns an error before any stage runs. UNLIKE the post-ship best-effort prune inside Send, every failure here is recorded as a failed stage — a user-invoked prune must not hide errors — and, mirroring Run, a failed result yields a non-nil error. A cancelled context short-circuits before the next stage.
func (*Manager) Run ¶
Run executes the pipeline per subvolume — Create → Prune → Send for each shipper — accumulating a RunResult. A failed stage is recorded and the run continues so other subvolumes/ships are still attempted; a cancelled context short-circuits before the next subvolume. The Notifier hook is invoked exactly once with the final result.
func (*Manager) Subvolumes ¶
Subvolumes exposes the configured subvolumes (for CLI iteration/reporting).
type ManagerOption ¶
type ManagerOption func(*managerOptions)
ManagerOption configures NewManager.
func WithManagerLogger ¶
func WithManagerLogger(l *slog.Logger) ManagerOption
WithManagerLogger hands the Manager the invocation's logger, which it passes to every engine, ship and notifier it builds. Without it they discard their warnings.
type Notifier ¶
Notifier reports a completed run. The no-op default, the ntfy, healthchecks, webhook and email drivers are all composed behind multiNotifier by newNotifier.
type NotifyConfig ¶
type NotifyConfig struct {
On []string `toml:"on,omitempty"`
Ntfy NtfyConfig `toml:"ntfy,omitempty"`
Healthchecks HealthchecksConfig `toml:"healthchecks,omitempty"`
Webhook WebhookConfig `toml:"webhook,omitempty"`
Email EmailConfig `toml:"email,omitempty"`
}
NotifyConfig selects and configures notification backends. On filters which run outcomes notify — a subset of {"success", "failure"}; an empty On means failure only (see shouldNotify). Each sub-table, when populated, activates one driver; the configured drivers fan out behind the Notifier interface.
type NtfyConfig ¶
type NtfyConfig struct {
URL string `toml:"url,omitempty"`
}
NtfyConfig configures the ntfy driver. URL is the topic URL. The auth token is not a config field: it resolves from BENTOO_NTFY_TOKEN via the secrets chain (env → user file → system file) and, when set, is sent via the Authorization header and is never logged.
type PipeStage ¶
PipeStage is one command in the archive pipe: a program name and its argv. It is the unit the pure builder emits and the executor feeds through the Runner.
type Piper ¶
Piper is the optional streaming seam of a Runner. runPipe requires it: a multi-stage pipe such as `btrfs send | zstd | rclone rcat` must not hold any stage's whole output in memory, which a Run-only Runner cannot avoid.
type RemoteGroup ¶
RemoteGroup is one remote source's contribution to `list --remote`. Label names the source for display ("btrbk targets", or the restic ship's name); Err, when non-nil, marks the source as failed — its listing is absent but the other groups are unaffected (lenient, read-only).
type RestoreOptions ¶
type RestoreOptions struct {
Driver string // "archive" | "restic" (resolved upstream from the --ship entry)
Yes bool // --yes: skip the confirm prompt
Confirm confirmFunc // nil → defaultConfirmFunc
Run Runner // nil → defaultRunner()
// archive:
Remote string // rclone remote+path prefix, e.g. "gdrive:bentoo-backups"
Compress string // decompressor program; default "zstd" → `zstd -d`
// Chain is the ordered full→target object chain to replay, RESOLVED UPSTREAM
// by the CLI/caller. Reconstructing the chain from remote object metadata is
// future work; Restore validates, orders and refuses-before-receive on this
// already-resolved chain.
Chain []chainLink
// restic:
Repo, PasswordFile string // non-secret locators: repo URL + password-FILE PATH
Include string // optional single file/subdir for a granular restic restore
}
RestoreOptions configures a Restore. Driver selects the path; Yes/Confirm gate the destructive action; Run is the subprocess seam. The remaining fields are split by driver — Remote/Compress/Chain drive the archive replay, Repo/PasswordFile/Include drive the restic restore.
type Retention ¶
type Retention struct {
Hourly int `toml:"hourly,omitempty"`
Daily int `toml:"daily,omitempty"`
Weekly int `toml:"weekly,omitempty"`
Monthly int `toml:"monthly,omitempty"`
PreserveMin string `toml:"preserve_min,omitempty"`
}
Retention is a GFS-style retention policy delegated to the native engine (mapped to btrbk snapshot_preserve/target_preserve). The counts are how many of each interval to keep; PreserveMin is btrbk's snapshot_preserve_min (e.g. "latest" or "2d") — left as a string because btrbk accepts both a keyword and a duration there.
type RollbackOptions ¶
type RollbackOptions struct {
Yes bool // --yes: skip the confirm prompt
Confirm confirmFunc // nil → defaultConfirmFunc
Run Runner // nil → defaultRunner()
}
RollbackOptions configures a Rollback. Yes/Confirm gate the destructive action; Run is the subprocess seam. Conventions mirror RestoreOptions: nil Confirm → defaultConfirmFunc, nil Run → defaultRunner().
type RunResult ¶
type RunResult struct {
StartedAt time.Time `json:"started_at"`
Duration time.Duration `json:"duration"`
Stages []StageResult `json:"stages"`
Err string `json:"error,omitempty"`
}
RunResult is the accumulated outcome of a Manager.Run, persisted so `status` can report the last run. A non-empty Err, or any failed stage, marks the run as failed; partial results (stages completed before a failure) are preserved.
func LoadLastRun ¶
LoadLastRun reads the most recent persisted RunResult.
func LoadRunResult ¶
LoadRunResult reads and decodes a RunResult from path.
func (*RunResult) AddStage ¶
func (r *RunResult) AddStage(s StageResult)
AddStage appends a stage outcome to the result.
func (*RunResult) Failed ¶
Failed reports whether the run encountered any error — a top-level Err or any stage with StatusFailed.
func (*RunResult) Save ¶
Save writes the result to path as indented JSON, atomically (temp + rename).
func (*RunResult) SaveLastRun ¶
SaveLastRun persists the result to LastRunPath.
type Runner ¶
type Runner interface {
Run(ctx context.Context, name string, args []string, stdin []byte) (stdout []byte, err error)
}
Runner is the subprocess seam shared by the engine and shipper drivers. Every external command goes through Run, or through the optional Piper seam for a streamed multi-stage pipe; both bind each process to ctx via exec.CommandContext so a cancelled parent kills the child. stdin is piped on the process's standard input, never placed in argv.
func NewReportingRunner ¶
NewReportingRunner returns a production Runner that emits stage/done progress events to r (keyed by id) for every command it runs. A nil reporter is normalized to a no-op.
It has no production caller yet: cmd/bentoo's deps.snapshotRunner is nil in production. It stays because it is the only place that sets execRunner.reporter and execRunner.taskID. execRunner is unexported, so nothing outside the package can build a reporting Runner, and without this constructor both fields stay zero and Run's progress-event path is dead code. Wiring it is one assignment: deps.snapshotRunner already reaches every driver through NewManager, newEngine, newShipper and newScheduler, so NewReportingRunner(rep, id) there turns a snapshot run's subprocesses into stage/done events. TestSnapshotRunnerEmitsStageDone and TestSnapshotRunnerReportsFailure prove that Run emits stage:<id>:<name> before a command and done:<id>:<ok> after it, on success and on failure.
type SMTPConfig ¶
type SMTPConfig struct {
Host string `toml:"host,omitempty"`
Port int `toml:"port,omitempty"`
User string `toml:"user,omitempty"`
}
SMTPConfig is the optional SMTP transport of the email driver. Host selects SMTP over local sendmail and is joined with Port as host:port. Host, Port and User stay in snapshot.toml because they are configuration, not secrets.
The password is deliberately NOT a field here: it resolves from BENTOO_SMTP_PASSWORD via the secrets chain (env → user file → system file) and is never read from snapshot.toml. PLAIN auth is enabled only when User is set AND that lookup yields a non-empty value; either one missing means the message is sent unauthenticated. The resolved password is never placed in argv, error strings, or logs.
type ScheduleConfig ¶
type ScheduleConfig struct {
Backend string `toml:"backend,omitempty"`
OnCalendar string `toml:"on_calendar,omitempty"`
Persistent *bool `toml:"persistent,omitempty"`
RandomizedDelay string `toml:"randomized_delay,omitempty"`
}
ScheduleConfig configures unit generation. Backend selects the scheduler driver ("systemd"; empty = no scheduling). Persistent is tri-state (*bool) so an unset value is distinguishable from an explicit false.
type Scheduler ¶
type Scheduler interface {
Apply(ctx context.Context, cfg ScheduleConfig) error
Remove(ctx context.Context) error
}
Scheduler installs and removes the periodic trigger for `bentoo snapshot run`. The systemd driver renders a .service + .timer. Apply is idempotent: re-running reconciles the units without creating duplicates.
type ShipConfig ¶
type ShipConfig struct {
Name string `toml:"name,omitempty"`
Type string `toml:"type"`
Target string `toml:"target,omitempty"`
// restic shipper. Repo is the restic repository URL; PasswordFile is the
// path to the repo password (kept out of the file itself); Compression
// maps to restic's --compression; MountStrategy selects how the snapshot
// is exposed to the restic backup (e.g. bind vs read-only mount).
Repo string `toml:"repo,omitempty"`
PasswordFile string `toml:"password_file,omitempty"`
Compression string `toml:"compression,omitempty"`
MountStrategy string `toml:"mount_strategy,omitempty"`
// archive shipper. Remote is the rclone remote target (remote:path); Mode
// selects full vs incremental archiving; Compress selects the stream codec
// applied before upload.
Remote string `toml:"remote,omitempty"`
Mode string `toml:"mode,omitempty"`
Compress string `toml:"compress,omitempty"`
}
ShipConfig is one replication target ([[ship]] array-of-tables). Type selects the shipper driver; Target is the destination (for ssh: user@host:/path). The remaining fields are driver-specific: the restic group (Repo..MountStrategy) applies when type = "restic", the archive group (Remote..Compress) when type = "archive".
type ShipReport ¶
type ShipReport struct {
Target string // replication destination (e.g. user@host:/path)
Snapshot string // ID/path of the snapshot shipped
Delegated bool // true when the transfer is performed by the engine (btrbk)
Note string // human-readable detail
Incremental bool // archive: true when sent with -p (incremental)
}
ShipReport is the outcome of a single Shipper.Send. For the ssh shipper the actual byte transfer is delegated to btrbk during engine Create, so Delegated is true and Send only records which target was (or will be) served.
type Shipper ¶
type Shipper interface {
Name() string
Send(ctx context.Context, snap Snapshot) (ShipReport, error)
}
Shipper replicates a snapshot to a remote target. Drivers are selected from ship.type via newShipper.
type Snapshot ¶
type Snapshot struct {
ID string
Subvolume string
Path string
CreatedAt time.Time
ReadOnly bool
ParentID string // "" = full
}
Snapshot describes a single point-in-time btrfs snapshot. ParentID is empty for a full snapshot and set to the parent's ID for an incremental one.
type StageResult ¶
type StageResult struct {
Subvolume string `json:"subvolume"`
Stage string `json:"stage"`
Target string `json:"target,omitempty"` // ship target name, for StageShip
Status string `json:"status"`
StartedAt time.Time `json:"started_at"`
Duration time.Duration `json:"duration"`
Err string `json:"error,omitempty"`
}
StageResult records the outcome of a single pipeline stage for one subvolume. Err is a string (not error) so the result round-trips through JSON for `status`.
type WebhookConfig ¶
type WebhookConfig struct {
URL string `toml:"url,omitempty"`
Headers map[string]string `toml:"headers,omitempty"`
}
WebhookConfig configures the generic webhook driver. URL receives a POST with the serialized RunResult; Headers are applied to the request. Header values holding secrets are never logged.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package snapshottest provides test doubles for package snapshot.
|
Package snapshottest provides test doubles for package snapshot. |