Release URBM 2026.06.15.r009
This commit is contained in:
@@ -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"`
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user