Release URBM 2026.06.15.r009

This commit is contained in:
Mikei386
2026-06-15 07:21:48 +02:00
parent 5e68a50373
commit 772358578e
12 changed files with 78 additions and 16 deletions
+1
View File
@@ -52,6 +52,7 @@ type Job struct {
Schedule Schedule `json:"schedule"`
Consistency Consistency `json:"consistency"`
Compression string `json:"compression"`
CPUCores int `json:"cpuCores,omitempty"`
Excludes []string `json:"excludes,omitempty"`
Tags []string `json:"tags,omitempty"`
FlashImage bool `json:"flashImage,omitempty"`
+3
View File
@@ -127,6 +127,9 @@ func ValidateJob(j Job) error {
if j.Compression != "auto" && j.Compression != "off" && j.Compression != "max" {
return errors.New("compression must be auto, off, or max")
}
if j.CPUCores < 0 || j.CPUCores > 256 {
return errors.New("cpuCores must be between 0 and 256")
}
if j.Consistency.Mode != "live" && j.Consistency.Mode != "stop" {
return errors.New("consistency mode must be live or stop")
}
+12
View File
@@ -79,3 +79,15 @@ func TestRetentionRequiresAtLeastOneRule(t *testing.T) {
t.Fatalf("age retention rejected: %v", err)
}
}
func TestBackupCPUCoresRange(t *testing.T) {
job := Job{SchemaVersion: 1, ID: "job", Name: "Share", Type: JobShare, RepositoryID: "repo", Sources: []Source{{Path: "/mnt/user/data"}}, Consistency: Consistency{Mode: "live"}, Compression: "auto", Retention: Retention{KeepWithinDays: 30}}
job.CPUCores = 2
if err := ValidateJob(job); err != nil {
t.Fatalf("valid CPU limit rejected: %v", err)
}
job.CPUCores = 257
if err := ValidateJob(job); err == nil {
t.Fatal("excessive CPU limit accepted")
}
}
+18 -7
View File
@@ -115,7 +115,7 @@ func (r *Runner) Backup(ctx context.Context, repo model.Repository, job model.Jo
}
args = append(args, sources...)
var summary Summary
err := r.run(ctx, repo, args, func(line []byte) {
err := r.runWithEnv(ctx, repo, args, func(line []byte) {
var status Progress
if json.Unmarshal(line, &status) == nil && status.MessageType == "status" {
if progress != nil {
@@ -127,7 +127,7 @@ func (r *Runner) Backup(ctx context.Context, repo model.Repository, job model.Jo
if json.Unmarshal(line, &candidate) == nil && candidate.MessageType == "summary" {
summary = candidate
}
}, nil)
}, nil, backupEnvironment(job))
return summary, err
}
@@ -137,7 +137,7 @@ func (r *Runner) BackupImage(ctx context.Context, repo model.Repository, job mod
args = append(args, "--tag", tag)
}
var summary Summary
err := r.runWithInput(ctx, repo, args, func(line []byte) {
err := r.runWithInputAndEnv(ctx, repo, args, func(line []byte) {
var status Progress
if json.Unmarshal(line, &status) == nil && status.MessageType == "status" {
if progress != nil {
@@ -149,7 +149,7 @@ func (r *Runner) BackupImage(ctx context.Context, repo model.Repository, job mod
if json.Unmarshal(line, &candidate) == nil && candidate.MessageType == "summary" {
summary = candidate
}
}, nil, image)
}, nil, image, backupEnvironment(job))
return summary, err
}
@@ -245,10 +245,14 @@ func (r *Runner) Restore(ctx context.Context, repo model.Repository, task model.
}
func (r *Runner) run(ctx context.Context, repo model.Repository, args []string, onLine func([]byte), capture *[]byte) error {
return r.runWithInput(ctx, repo, args, onLine, capture, nil)
return r.runWithInputAndEnv(ctx, repo, args, onLine, capture, nil, nil)
}
func (r *Runner) runWithInput(ctx context.Context, repo model.Repository, args []string, onLine func([]byte), capture *[]byte, input io.Reader) error {
func (r *Runner) runWithEnv(ctx context.Context, repo model.Repository, args []string, onLine func([]byte), capture *[]byte, environment []string) error {
return r.runWithInputAndEnv(ctx, repo, args, onLine, capture, nil, environment)
}
func (r *Runner) runWithInputAndEnv(ctx context.Context, repo model.Repository, args []string, onLine func([]byte), capture *[]byte, input io.Reader, environment []string) error {
password, err := r.Secrets.Get(repo.PasswordRef)
if err != nil {
return fmt.Errorf("authentication: load repository password: %w", err)
@@ -319,7 +323,7 @@ func (r *Runner) runWithInput(ctx context.Context, repo model.Repository, args [
}
return nil
}
cmd.Env = []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HOME=/root", "RESTIC_PASSWORD_FILE=" + passwordPath}
cmd.Env = append([]string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HOME=/root", "RESTIC_PASSWORD_FILE=" + passwordPath}, environment...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
@@ -367,6 +371,13 @@ func (r *Runner) runWithInput(ctx context.Context, repo model.Repository, args [
return nil
}
func backupEnvironment(job model.Job) []string {
if job.CPUCores <= 0 {
return nil
}
return []string{"GOMAXPROCS=" + fmt.Sprint(job.CPUCores)}
}
func resticError(message string, commandErr error) error {
lower := strings.ToLower(message)
switch {
+23
View File
@@ -51,6 +51,29 @@ func TestBackupUsesPasswordFileAndStructuredArguments(t *testing.T) {
}
}
func TestBackupLimitsCPUForThisJob(t *testing.T) {
dir := t.TempDir()
envPath := filepath.Join(dir, "gomaxprocs")
script := filepath.Join(dir, "restic")
body := fmt.Sprintf("#!/bin/sh\nprintf '%%s' \"$GOMAXPROCS\" > '%s'\nprintf '%%s\\n' '{\"message_type\":\"summary\",\"snapshot_id\":\"cpu123\"}'\n", envPath)
if err := os.WriteFile(script, []byte(body), 0700); err != nil {
t.Fatal(err)
}
runner := &Runner{Binary: script, RuntimeDir: dir, Secrets: fakeSecrets{"password": "secret"}}
repo := model.Repository{Type: model.RepositoryLocal, Location: "/repo", PasswordRef: "password"}
job := model.Job{ID: "job", Compression: "auto", CPUCores: 2}
if _, err := runner.Backup(context.Background(), repo, job, []string{"/source"}, nil); err != nil {
t.Fatal(err)
}
value, err := os.ReadFile(envPath)
if err != nil {
t.Fatal(err)
}
if string(value) != "2" {
t.Fatalf("GOMAXPROCS = %q", value)
}
}
func TestBackupImageStreamsRawDeviceWithStableFilename(t *testing.T) {
dir := t.TempDir()
argsPath := filepath.Join(dir, "args")