f0dfa689fe
Three small follow-ups from review:
1. Restore target is now operator-editable. Default value is the
literal '\$HOME/rm-restore/<job-id>/' (agent expands \$HOME at
run time using os.UserHomeDir(); also handles \${HOME} and ~/
prefixes). Operator can replace with any absolute path.
- ui_restore.go validates the input is either absolute or starts
with one of the recognised prefixes; other env-var refs (\$PATH
etc.) are deliberately rejected so operator paths can't pick up
arbitrary agent env values.
- host_restore.html replaces the read-only mono-text display with
a real <input>; help text spells out that \$HOME resolves
agent-side and <job-id> is substituted on dispatch.
- install.sh + the systemd unit prep /root/rm-restore so the
default works under the sandbox: ReadWritePaths gains a soft
'-/root/rm-restore' entry (the '-' makes the bind-mount soft-fail
if missing, but install.sh pre-creates it root-owned 0700).
2. --no-ownership flag now gated on restic version. The flag was
added in restic 0.17 and 0.16 rejects it. Previously dropped it
wholesale — that meant new-dir restores silently preserved
ownership against design intent on 0.17+. Now the agent threads
its detected restic version (sysinfo already collects it) through
runner.Config -> restic.Env, and RunRestore appends --no-ownership
only when AtLeastVersion(0, 17) returns true. 0.16 hosts still
restore with original uid/gid; help text in the wizard explicitly
notes this. The previous 'Original ownership is preserved' copy
was wrong for new-dir mode and is corrected.
3. golangci-lint misspell locale switched US -> UK and the codebase
swept (73 corrections, mostly behaviour/serialise/recognise/honour).
Wire-format ErrorCode 'unauthorized' -> 'unauthorised' is a tiny
contract change but the agent doesn't parse those codes today and
no external API consumers exist yet. Tests passed before + after.
Tests:
- internal/restic/version_test.go covers Env.AtLeastVersion across
edge cases (empty, exact match, patch above, minor below, non-
numeric) and expandHome on \$HOME / \${HOME} / ~/, plus
pass-through for absolute paths and refusal of other env vars.
- ui_restore_test updated: TargetDir now starts '\$HOME/rm-restore/'
with the job_id substituted into the placeholder.
Live verified on the smoke env: default target restored to
/root/rm-restore/<job-id>/ as the agent's expanded \$HOME (2 files,
14 bytes); custom override '/tmp/custom-restore/<job-id>/' restored
into the agent's PrivateTmp namespace (1 file, 6 bytes); both jobs
'succeeded', exit 0.
79 lines
2.8 KiB
Go
79 lines
2.8 KiB
Go
// host_bandwidth_push.go — server → agent fan-out of host-wide
|
|
// bandwidth caps via config.update.
|
|
//
|
|
// Two entry points: pushBandwidthOnHello (called from onAgentHello,
|
|
// always pushes the current state so the agent picks up edits made
|
|
// while it was offline) and pushBandwidthToAgent (called after the
|
|
// PUT bandwidth handler succeeds, so an online agent re-arms within
|
|
// seconds).
|
|
//
|
|
// We always send pointer fields (zero-valued when uncapped) so the
|
|
// agent can distinguish "no change" (nil → field absent on the wire)
|
|
// from "explicitly cleared" (non-nil zero pointer). See
|
|
// api.ConfigUpdatePayload doc for the wire semantics.
|
|
package http
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
|
|
"gitea.dcglab.co.uk/steve/restic-manager/internal/server/ws"
|
|
)
|
|
|
|
// pushBandwidthOnHello ships the host's current bandwidth caps as a
|
|
// config.update on the supplied conn. Silent no-op on lookup error.
|
|
func (s *Server) pushBandwidthOnHello(ctx context.Context, hostID string, conn *ws.Conn) {
|
|
host, err := s.deps.Store.GetHost(ctx, hostID)
|
|
if err != nil {
|
|
slog.Warn("on-hello: load host for bandwidth", "host_id", hostID, "err", err)
|
|
return
|
|
}
|
|
payload := bandwidthPayload(host.BandwidthUpKBps, host.BandwidthDownKBps)
|
|
env, err := api.Marshal(api.MsgConfigUpdate, "", payload)
|
|
if err != nil {
|
|
slog.Error("on-hello: marshal bandwidth config.update", "host_id", hostID, "err", err)
|
|
return
|
|
}
|
|
sendCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
if err := conn.Send(sendCtx, env); err != nil {
|
|
slog.Warn("on-hello: send bandwidth config.update", "host_id", hostID, "err", err)
|
|
}
|
|
}
|
|
|
|
// pushBandwidthToAgent ships the supplied caps via the hub. Caller is
|
|
// expected to check Hub.Connected first when it matters.
|
|
func (s *Server) pushBandwidthToAgent(ctx context.Context, hostID string, up, down *int) error {
|
|
env, err := api.Marshal(api.MsgConfigUpdate, "", bandwidthPayload(up, down))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sendCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
return s.deps.Hub.Send(sendCtx, hostID, env)
|
|
}
|
|
|
|
// bandwidthPayload builds a ConfigUpdatePayload with only the
|
|
// bandwidth fields populated. Pointers are passed through verbatim;
|
|
// callers wanting to clear a cap should pass a non-nil pointer to 0.
|
|
// On the on-hello path we materialise zero-valued pointers when the
|
|
// host record has no cap set, so the agent's stored state is always
|
|
// in sync (rather than retaining whatever value it last received).
|
|
func bandwidthPayload(up, down *int) api.ConfigUpdatePayload {
|
|
zero := 0
|
|
upPtr := up
|
|
if upPtr == nil {
|
|
upPtr = &zero
|
|
}
|
|
downPtr := down
|
|
if downPtr == nil {
|
|
downPtr = &zero
|
|
}
|
|
return api.ConfigUpdatePayload{
|
|
BandwidthUpKBps: upPtr,
|
|
BandwidthDownKBps: downPtr,
|
|
}
|
|
}
|