Repository: creativeprojects/resticprofile (language: python)

Issue:
Nice in systemd unit is always 5
Hello 
I noticed that `nice` in global section is not correctly [passed](https://github.com/creativeprojects/resticprofile/blob/3476fbce1425f3368527c05a9130d511fe530139/systemd/generate.go#L31) to systemd unit template. 

for example with this config

```yaml
version: "1"
global:
  nice: 17
default:
  repository: "my_repo"
  password-file: "key"
  backup:
    exclude-file: "excludes"
    exclude-caches: true
    one-file-system: true
    tag:
      - "test"
    source:
      - "/tmp/"
    schedule: "*-*-* 12:15:00"
    schedule-permission: system
    schedule-priority: background
    schedule-lock-mode: default
    schedule-lock-wait: 15m30s
 ```
After running `schedule` the systemd unit file is 

```
cat /etc/systemd/system/resticprofile-backup@profile-default.service
[Unit]
Description=resticprofile backup for profile default in ./profiles.yaml

[Service]
Type=notify
WorkingDirectory=/tmp
ExecStart=/usr/local/bin/resticprofile --no-prio --no-ansi --config ./profiles.yaml --name default --lock-wait 15m30s backup
Nice=5
Environment="HOME=/root"
Environment="SUDO_USER=test"
```
Nice is set to 5 and not to 17 as expected



Discussion / hints:
This is right, I can see in `systemd` that nice is set to a predefined value:
https://github.com/creativeprojects/resticprofile/blob/3476fbce1425f3368527c05a9130d511fe530139/systemd/generate.go#L121

Thanks for the report 👍🏻 
Hello,
Just wanted to chime in that when starting resticprofile from the terminal with:

```
# leaving out definitely unrelated settings, i.e. paths
global:
  # neither this:
  priority: "idle"
  # nor this:
  nice: 19
  # ...nor both together change anything.

default:
  backup:
    verbose: 2
```
also will create restic instances with the default niceness.

- `resticprofile version`: resticprofile version 0.24.0 commit bcfaaa7cacfa7f88a94d5dc62ca69b2d89205d89
- `restic version`: restic 0.16.2 compiled with go1.21.3 on linux/amd64

Edit: resticprofile itself respects the setting, only the restic instances are started at the default niceness.
> will create restic instances with the default niceness

Can confirm, I'm seeing this too.

* resticprofile version 0.26.0 commit 2ab1f3a38467114aeb4095c0924817b369280e39
* restic 0.16.4 compiled with go1.21.6 on linux/amd64
* Linux 6.1.0-17-amd64 SMP PREEMPT_DYNAMIC Debian 6.1.69-1 (2023-12-30) x86_64 (bookworm)

Now, I'm no Go programmer, and only somewhat familiar with the Unix/Linux API, but it looks to me as if you're creating a process group, but then setting the niceness only for the current process and not the group in https://github.com/creativeprojects/resticprofile/blob/master/priority/linux.go#L52-L60:

```go
err = unix.Setpriority(unix.PRIO_PROCESS, pid, priority)
```

Could using `PRIO_PGRP` instead of `PRIO_PROCESS` be enough to fix that?
> Could using PRIO_PGRP instead of PRIO_PROCESS be enough to fix that?

Well spotted!

If you guys are ok with compiling go code, can you try again with this MR? https://github.com/creativeprojects/resticprofile/pull/339

(This is a fix when running from the terminal, not from `systemd`)

Hello again!

#339 seems to work for me!
> (This is a fix when running from the terminal, not from `systemd`)

Can confirm that this indeed fixes it, but only when not running from a systemd unit (i.e. scheduled). In other words, scheduled runs are still broken.
@creativeprojects any progress on that year old very annoying bug, while running from systemd?

using v.28 and my (elderly) laptop – since it gained a new, fast NVMe –  gets always unresponsive while checking/backing up.
```
priority = "Idle" # tried 19, Background, etc…
ionice = true
ionice-class = 3
ionice-level = 7
```



Fix patch:
diff --git a/.gitignore b/.gitignore
index 2f67aba0..e12be48b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,5 @@ status.json
 
 go.work
 go.work.sum
+
+/*.txt
diff --git a/Makefile b/Makefile
index 1cea6565..6247f6c8 100644
--- a/Makefile
+++ b/Makefile
@@ -309,3 +309,17 @@ fix:
 	GOOS=darwin golangci-lint run --fix
 	GOOS=linux golangci-lint run --fix
 	GOOS=windows golangci-lint run --fix
+
+.PHONY: deploy-current
+deploy-current: build-linux build-pi
+	@echo "[*] $@"
+	for server in $$(cat targets_amd64.txt); do \
+		echo "Deploying to $$server" ; \
+		rsync -avz --progress $(BINARY_LINUX_AMD64) $$server: ; \
+		ssh $$server "sudo -S install $(BINARY_LINUX_AMD64) /usr/local/bin/resticprofile" ; \
+	done
+	for server in $$(cat targets_armv6.txt); do \
+		echo "Deploying to $$server" ; \
+		rsync -avz --progress $(BINARY_PI) $$server: ; \
+		ssh $$server "sudo -S install $(BINARY_PI) /usr/local/bin/resticprofile" ; \
+	done
diff --git a/config/global.go b/config/global.go
index 91108061..d3da9f51 100644
--- a/config/global.go
+++ b/config/global.go
@@ -9,9 +9,9 @@ import (
 
 // Global holds the configuration from the global section
 type Global struct {
-	IONice               bool                `mapstructure:"ionice" default:"false" description:"Enables setting the unix IO priority class and level for resticprofile and child processes (only on unix OS)."`
-	IONiceClass          int                 `mapstructure:"ionice-class" default:"2" range:"[1:3]" description:"Sets the unix \"ionice-class\" to apply when \"ionice\" is enabled"`
-	IONiceLevel          int                 `mapstructure:"ionice-level" default:"0" range:"[0:7]" description:"Sets the unix \"ionice-level\" to apply when \"ionice\" is enabled"`
+	IONice               bool                `mapstructure:"ionice" default:"false" description:"Enables setting the linux IO priority class and level for resticprofile and child processes (only on linux OS)."`
+	IONiceClass          int                 `mapstructure:"ionice-class" default:"2" range:"[1:3]" description:"Sets the linux \"ionice-class\" (I/O scheduling class) to apply when \"ionice\" is enabled (1=realtime, 2=best-effort, 3=idle)"`
+	IONiceLevel          int                 `mapstructure:"ionice-level" default:"0" range:"[0:7]" description:"Sets the linux \"ionice-level\" (I/O priority within the scheduling class) to apply when \"ionice\" is enabled (0=highest priority, 7=lowest priority)"`
 	Nice                 int                 `mapstructure:"nice" default:"0" range:"[-20:19]" description:"Sets the unix \"nice\" value for resticprofile and child processes (on any OS)"`
 	Priority             string              `mapstructure:"priority" default:"normal" enum:"idle;background;low;normal;high;highest" description:"Sets process priority class for resticprofile and child processes (on any OS)"`
 	DefaultCommand       string              `mapstructure:"default-command" default:"snapshots" description:"The restic or resticprofile command to use when no command was specified"`
@@ -20,8 +20,8 @@ type Global struct {
 	ResticBinary         string              `mapstructure:"restic-binary" description:"Full path of the restic executable (detected if not set)"`
 	ResticVersion        string              // not configurable at the moment. To be set after ResticBinary is known.
 	FilterResticFlags    bool                `mapstructure:"restic-arguments-filter" default:"true" description:"Remove unknown flags instead of passing all configured flags to restic"`
-	ResticLockRetryAfter time.Duration       `mapstructure:"restic-lock-retry-after" default:"1m" description:"Time to wait before trying to get a lock on a restic repositoey - see https://creativeprojects.github.io/resticprofile/usage/locks/"`
-	ResticStaleLockAge   time.Duration       `mapstructure:"restic-stale-lock-age" default:"1h" description:"The age an unused lock on a restic repository must have at least before resiticprofile attempts to unlock - see https://creativeprojects.github.io/resticprofile/usage/locks/"`
+	ResticLockRetryAfter time.Duration       `mapstructure:"restic-lock-retry-after" default:"1m" description:"Time to wait before trying to get a lock on a restic repository - see https://creativeprojects.github.io/resticprofile/usage/locks/"`
+	ResticStaleLockAge   time.Duration       `mapstructure:"restic-stale-lock-age" default:"1h" description:"The age an unused lock on a restic repository must have at least before resticprofile attempts to unlock - see https://creativeprojects.github.io/resticprofile/usage/locks/"`
 	ShellBinary          []string            `mapstructure:"shell" default:"auto" examples:"sh;bash;pwsh;powershell;cmd" description:"The shell that is used to run commands (default is OS specific)"`
 	MinMemory            uint64              `mapstructure:"min-memory" default:"100" description:"Minimum available memory (in MB) required to run any commands - see https://creativeprojects.github.io/resticprofile/usage/memory/"`
 	Scheduler            string              `mapstructure:"scheduler" default:"auto" examples:"auto;launchd;systemd;taskscheduler;crond;crond:/usr/bin/crontab;crontab:*:/etc/cron.d/resticprofile" description:"Selects the scheduler. Blank or \"auto\" uses the default scheduler of your operating system: \"launchd\", \"systemd\", \"taskscheduler\" or \"crond\" (as fallback). Alternatively you can set \"crond\" for cron compatible schedulers supporting the crontab executable API or \"crontab:[user:]file\" to write into a crontab file directly. The need for a user is detected if missing and can be set to a name, \"-\" (no user) or \"*\" (current user)."`
diff --git a/docs/content/configuration/priority.md b/docs/content/configuration/priority.md
index b942f9e1..0f4643a1 100644
--- a/docs/content/configuration/priority.md
+++ b/docs/content/configuration/priority.md
@@ -10,7 +10,7 @@ You can lower the priority of restic to avoid slowing down other processes. This
 
 ## Nice
 
-You can use these values for the `priority` parameter:
+You can use these values for the `priority` parameter, string or numeric values are both valid:
 
 | String value | "nice" equivalent on unixes | Notes |
 |--------------|-----------------------------|------|
diff --git a/docs/content/schedules/systemd.md b/docs/content/schedules/systemd.md
index 07efc3d5..14cb0b03 100644
--- a/docs/content/schedules/systemd.md
+++ b/docs/content/schedules/systemd.md
@@ -1,7 +1,7 @@
 ---
 title: "Systemd"
 weight: 105
-tags: ["v0.25.0"]
+tags: ["v0.25.0", "v0.29.0"]
 ---
 
 
@@ -48,6 +48,7 @@ Specifying the profile option `schedule-after-network-online: true` means that t
 for a network connection before running.
 This is done via an [After=network-online.target](https://systemd.io/NETWORK_ONLINE/) entry in the service.
 
+
 ## systemd drop-in files
 
 It is possible to automatically populate `*.conf.d`
@@ -158,6 +159,22 @@ pass = $(systemd-ask-password -n "smb restic user password" | rclone obscure -)
 EOF
 ```
 
+## Priority and CPU scheduling
+
+resticprofile allows you to set the `nice` value, the CPU scheduling policy and IO nice values for the systemd service.
+This is only working properly for resticprofile >= 0.29.0.
+
+| systemd unit option  | resticprofile option |
+|----------------------|----------------------|
+| CPUSchedulingPolicy  | set to `idle` if schedule `priority` = `background` , otherwise default to standard policy |
+| Nice                 | `nice` from `global` section |
+| IOSchedulingClass    | `ionice-class` from `global` section |
+| IOSchedulingPriority | `ionice-level` from `global` section |
+
+{{% notice note %}}
+When setting the `CPUSchedulingPolicy` to `idle` (by setting `priority` to `background`), the backup might never execute if all your CPU cores are always busy.
+{{% /notice %}}
+
 ## How to change the default systemd unit and timer file using a template
 
 By default, an opinionated systemd unit and timer are automatically generated by resticprofile.
@@ -219,12 +236,18 @@ Here are the defaults if you don't specify your own (which I recommend to use as
 Description={{ .JobDescription }}
 {{ if .AfterNetworkOnline }}After=network-online.target
 {{ end }}
-
 [Service]
 Type=notify
 WorkingDirectory={{ .WorkingDirectory }}
 ExecStart={{ .CommandLine }}
-{{ if .Nice }}Nice={{ .Nice }}{{ end }}
+{{ if .Nice }}Nice={{ .Nice }}
+{{ end -}}
+{{ if .CPUSchedulingPolicy }}CPUSchedulingPolicy={{ .CPUSchedulingPolicy }}
+{{ end -}}
+{{ if .IOSchedulingClass }}IOSchedulingClass={{ .IOSchedulingClass }}
+{{ end -}}
+{{ if .IOSchedulingPriority }}IOSchedulingPriority={{ .IOSchedulingPriority }}
+{{ end -}}
 {{ range .Environment -}}
 Environment="{{ . }}"
 {{ end -}}
diff --git a/examples/linux.yaml b/examples/linux.yaml
index e67d6447..ffc80720 100644
--- a/examples/linux.yaml
+++ b/examples/linux.yaml
@@ -8,6 +8,10 @@ global:
     priority: low
     systemd-unit-template: sample.service
     prevent-sleep: false
+    ionice: true
+    ionice-class: 3
+    ionice-level: 7
+    nice: 19
 
 default:
     password-file: key
@@ -57,12 +61,12 @@ test2:
     backup:
         source: ./
         schedule: "*:05,20,35,50"
-        schedule-permission: system
+        schedule-permission: user
         schedule-log: backup-test2.log
         run-after: "chown -R $SUDO_USER $HOME/.cache/restic /tmp/backup"
     check:
         schedule: "*-*-2"
-        schedule-permission: system
+        schedule-permission: user
         schedule-log: check-test2.log
 
 test3:
diff --git a/examples/sample.service b/examples/sample.service
index 4cb59b55..747ccd21 100644
--- a/examples/sample.service
+++ b/examples/sample.service
@@ -6,7 +6,14 @@ OnFailure=unit-status-mail@%n.service
 Type=notify
 WorkingDirectory={{ .WorkingDirectory }}
 ExecStart={{ .CommandLine }}
-{{ if .Nice }}Nice={{ .Nice }}{{ end }}
+{{ if .Nice }}Nice={{ .Nice }}
+{{ end -}}
+{{ if .CPUSchedulingPolicy }}CPUSchedulingPolicy={{ .CPUSchedulingPolicy }}
+{{ end -}}
+{{ if .IOSchedulingClass }}IOSchedulingClass={{ .IOSchedulingClass }}
+{{ end -}}
+{{ if .IOSchedulingPriority }}IOSchedulingPriority={{ .IOSchedulingPriority }}
+{{ end -}}
 {{ range .Environment -}}
 Environment="{{ . }}"
 {{ end -}}
diff --git a/schedule/handler_systemd.go b/schedule/handler_systemd.go
index ded48e18..55023407 100644
--- a/schedule/handler_systemd.go
+++ b/schedule/handler_systemd.go
@@ -107,20 +107,23 @@ func (h *HandlerSystemd) CreateJob(job *Config, schedules []*calendar.Event, per
 	}
 
 	err := systemd.Generate(systemd.Config{
-		CommandLine:        job.Command + " --no-prio " + strings.Join(job.Arguments, " "),
-		Environment:        job.Environment,
-		WorkingDirectory:   job.WorkingDirectory,
-		Title:              job.ProfileName,
-		SubTitle:           job.CommandName,
-		JobDescription:     job.JobDescription,
-		TimerDescription:   job.TimerDescription,
-		Schedules:          job.Schedules,
-		UnitType:           unitType,
-		Priority:           job.GetPriority(),
-		UnitFile:           h.config.UnitTemplate,
-		TimerFile:          h.config.TimerTemplate,
-		AfterNetworkOnline: job.AfterNetworkOnline,
-		DropInFiles:        job.SystemdDropInFiles,
+		CommandLine:          job.Command + " " + strings.Join(append([]string{"--no-prio"}, job.Arguments...), " "),
+		Environment:          job.Environment,
+		WorkingDirectory:     job.WorkingDirectory,
+		Title:                job.ProfileName,
+		SubTitle:             job.CommandName,
+		JobDescription:       job.JobDescription,
+		TimerDescription:     job.TimerDescription,
+		Schedules:            job.Schedules,
+		UnitType:             unitType,
+		Priority:             job.GetPriority(),
+		UnitFile:             h.config.UnitTemplate,
+		TimerFile:            h.config.TimerTemplate,
+		AfterNetworkOnline:   job.AfterNetworkOnline,
+		DropInFiles:          job.SystemdDropInFiles,
+		Nice:                 h.config.Nice,
+		IOSchedulingClass:    h.config.IONiceClass,
+		IOSchedulingPriority: h.config.IONiceLevel,
 	})
 	if err != nil {
 		return err
diff --git a/schedule/scheduler_config.go b/schedule/scheduler_config.go
index 8521dba5..3ea542a1 100644
--- a/schedule/scheduler_config.go
+++ b/schedule/scheduler_config.go
@@ -52,6 +52,9 @@ func (s SchedulerCrond) Convert(_ string) SchedulerConfig { return s }
 type SchedulerSystemd struct {
 	UnitTemplate  string
 	TimerTemplate string
+	Nice          int
+	IONiceClass   int
+	IONiceLevel   int
 }
 
 func (s SchedulerSystemd) Type() string                     { return constants.SchedulerSystemd }
@@ -70,6 +73,7 @@ func NewSchedulerConfig(global *config.Global) SchedulerConfig {
 		} else {
 			return SchedulerCrond{}
 		}
+
 	case constants.SchedulerCrontab:
 		if len(resource) > 0 {
 			if user, location, found := strings.Cut(resource, ":"); found {
@@ -85,27 +89,38 @@ func NewSchedulerConfig(global *config.Global) SchedulerConfig {
 		} else {
 			panic(fmt.Errorf("invalid schedule %q, no crontab file was specified, expecting \"%s: filename\"", scheduler, scheduler))
 		}
+
 	case constants.SchedulerLaunchd:
 		return SchedulerLaunchd{}
+
 	case constants.SchedulerSystemd:
-		return SchedulerSystemd{
-			UnitTemplate:  global.SystemdUnitTemplate,
-			TimerTemplate: global.SystemdTimerTemplate,
-		}
+		return getSchedulerSystemdDefaultConfig(global)
+
 	case constants.SchedulerWindows:
 		return SchedulerWindows{}
+
 	default:
 		return SchedulerDefaultOS{
 			defaults: []SchedulerConfig{
-				SchedulerSystemd{
-					UnitTemplate:  global.SystemdUnitTemplate,
-					TimerTemplate: global.SystemdTimerTemplate,
-				},
+				getSchedulerSystemdDefaultConfig(global),
 			},
 		}
 	}
 }
 
+func getSchedulerSystemdDefaultConfig(global *config.Global) SchedulerSystemd {
+	scheduler := SchedulerSystemd{
+		UnitTemplate:  global.SystemdUnitTemplate,
+		TimerTemplate: global.SystemdTimerTemplate,
+		Nice:          global.Nice,
+	}
+	if global.IONice {
+		scheduler.IONiceClass = global.IONiceClass
+		scheduler.IONiceLevel = global.IONiceLevel
+	}
+	return scheduler
+}
+
 var (
 	_ SchedulerConfig = SchedulerDefaultOS{}
 	_ SchedulerConfig = SchedulerCrond{}
diff --git a/systemd/generate.go b/systemd/generate.go
index 1535407c..672b8cb7 100644
--- a/systemd/generate.go
+++ b/systemd/generate.go
@@ -31,7 +31,14 @@ Description={{ .JobDescription }}
 Type=notify
 WorkingDirectory={{ .WorkingDirectory }}
 ExecStart={{ .CommandLine }}
-{{ if .Nice }}Nice={{ .Nice }}{{ end }}
+{{ if .Nice }}Nice={{ .Nice }}
+{{ end -}}
+{{ if .CPUSchedulingPolicy }}CPUSchedulingPolicy={{ .CPUSchedulingPolicy }}
+{{ end -}}
+{{ if .IOSchedulingClass }}IOSchedulingClass={{ .IOSchedulingClass }}
+{{ end -}}
+{{ if .IOSchedulingPriority }}IOSchedulingPriority={{ .IOSchedulingPriority }}
+{{ end -}}
 {{ range .Environment -}}
 Environment="{{ . }}"
 {{ end -}}
@@ -66,33 +73,40 @@ var fs afero.Fs
 // templateInfo to create systemd unit
 type templateInfo struct {
 	templates.DefaultData
-	JobDescription     string
-	TimerDescription   string
-	WorkingDirectory   string
-	CommandLine        string
-	OnCalendar         []string
-	SystemdProfile     string
-	Nice               int
-	Environment        []string
-	AfterNetworkOnline bool
+	JobDescription       string
+	TimerDescription     string
+	WorkingDirectory     string
+	CommandLine          string
+	OnCalendar           []string
+	SystemdProfile       string
+	Nice                 int
+	Environment          []string
+	AfterNetworkOnline   bool
+	CPUSchedulingPolicy  string
+	IOSchedulingClass    int
+	IOSchedulingPriority int
 }
 
 // Config for generating systemd unit and timer files
 type Config struct {
-	CommandLine        string
-	Environment        []string
-	WorkingDirectory   string
-	Title              string
-	SubTitle           string
-	JobDescription     string
-	TimerDescription   string
-	Schedules          []string
-	UnitType           UnitType
-	Priority           string
-	UnitFile           string
-	TimerFile          string
-	DropInFiles        []string
-	AfterNetworkOnline bool
+	CommandLine          string
+	Environment          []string
+	WorkingDirectory     string
+	Title                string
+	SubTitle             string
+	JobDescription       string
+	TimerDescription     string
+	Schedules            []string
+	UnitType             UnitType
+	Priority             string // standard or background
+	UnitFile             string
+	TimerFile            string
+	DropInFiles          []string
+	AfterNetworkOnline   bool
+	Nice                 int
+	CPUSchedulingPolicy  string
+	IOSchedulingClass    int
+	IOSchedulingPriority int
 }
 
 func init() {
@@ -123,22 +137,25 @@ func Generate(config Config) error {
 		environment = append(environment, fmt.Sprintf("SUDO_USER=%s", sudoUser))
 	}
 
-	nice := constants.DefaultBackgroundNiceFlag
-	if config.Priority == constants.SchedulePriorityStandard {
-		nice = constants.DefaultStandardNiceFlag
+	policy := ""
+	if config.Priority == constants.SchedulePriorityBackground {
+		policy = "idle"
 	}
 
 	info := templateInfo{
-		DefaultData:        templates.NewDefaultData(nil),
-		JobDescription:     config.JobDescription,
-		TimerDescription:   config.TimerDescription,
-		WorkingDirectory:   config.WorkingDirectory,
-		CommandLine:        config.CommandLine,
-		OnCalendar:         config.Schedules,
-		AfterNetworkOnline: config.AfterNetworkOnline,
-		SystemdProfile:     systemdProfile,
-		Nice:               nice,
-		Environment:        environment,
+		DefaultData:          templates.NewDefaultData(nil),
+		JobDescription:       config.JobDescription,
+		TimerDescription:     config.TimerDescription,
+		WorkingDirectory:     config.WorkingDirectory,
+		CommandLine:          config.CommandLine,
+		OnCalendar:           config.Schedules,
+		AfterNetworkOnline:   config.AfterNetworkOnline,
+		SystemdProfile:       systemdProfile,
+		Nice:                 config.Nice,
+		Environment:          environment,
+		CPUSchedulingPolicy:  policy,
+		IOSchedulingClass:    config.IOSchedulingClass,
+		IOSchedulingPriority: config.IOSchedulingPriority,
 	}
 
 	var data bytes.Buffer


Test patch:
diff --git a/crond/crontab_test.go b/crond/crontab_test.go
index baeead57..4f2fe45b 100644
--- a/crond/crontab_test.go
+++ b/crond/crontab_test.go
@@ -288,7 +288,7 @@ func TestUseCrontabBinary(t *testing.T) {
 	binary := platform.Executable("./crontab")
 	defer func() { _ = os.Remove(binary) }()
 
-	cmd := exec.Command("go", "build", "-o", binary, "./mock")
+	cmd := exec.Command("go", "build", "-buildvcs=false", "-o", binary, "./mock")
 	require.NoError(t, cmd.Run())
 
 	crontab := NewCrontab(nil)
diff --git a/schedule/scheduler_config_test.go b/schedule/scheduler_config_test.go
index 29589ac0..d833c01c 100644
--- a/schedule/scheduler_config_test.go
+++ b/schedule/scheduler_config_test.go
@@ -24,12 +24,53 @@ func TestOsDefaultConfig(t *testing.T) {
 }
 
 func TestSystemdConfig(t *testing.T) {
-	g := &config.Global{
-		Scheduler:            constants.SchedulerSystemd,
-		SystemdTimerTemplate: "timer.tpl",
-		SystemdUnitTemplate:  "unit.tpl",
+	testCases := []struct {
+		global   *config.Global
+		expected SchedulerSystemd
+	}{
+		{
+			global: &config.Global{
+				Scheduler:            constants.SchedulerSystemd,
+				SystemdTimerTemplate: "timer.tpl",
+				SystemdUnitTemplate:  "unit.tpl",
+			},
+			expected: SchedulerSystemd{TimerTemplate: "timer.tpl", UnitTemplate: "unit.tpl"},
+		},
+		{
+			global: &config.Global{
+				Scheduler:            constants.SchedulerSystemd,
+				SystemdTimerTemplate: "timer.tpl",
+				SystemdUnitTemplate:  "unit.tpl",
+				IONiceClass:          3,
+				IONiceLevel:          5,
+			},
+			expected: SchedulerSystemd{TimerTemplate: "timer.tpl", UnitTemplate: "unit.tpl"},
+		},
+		{
+			global: &config.Global{
+				Scheduler:            constants.SchedulerSystemd,
+				SystemdTimerTemplate: "timer.tpl",
+				SystemdUnitTemplate:  "unit.tpl",
+				IONice:               true,
+				IONiceClass:          3,
+				IONiceLevel:          5,
+			},
+			expected: SchedulerSystemd{TimerTemplate: "timer.tpl", UnitTemplate: "unit.tpl", IONiceClass: 3, IONiceLevel: 5},
+		},
+		{
+			global: &config.Global{
+				Scheduler:            constants.SchedulerSystemd,
+				SystemdTimerTemplate: "timer.tpl",
+				SystemdUnitTemplate:  "unit.tpl",
+				Nice:                 12,
+			},
+			expected: SchedulerSystemd{TimerTemplate: "timer.tpl", UnitTemplate: "unit.tpl", Nice: 12},
+		},
+	}
+
+	for _, tc := range testCases {
+		assert.Equal(t, tc.expected, NewSchedulerConfig(tc.global))
 	}
-	assert.Equal(t, SchedulerSystemd{TimerTemplate: "timer.tpl", UnitTemplate: "unit.tpl"}, NewSchedulerConfig(g))
 }
 
 func TestCrondConfig(t *testing.T) {
diff --git a/systemd/generate_test.go b/systemd/generate_test.go
index 96a53f7a..5ce024b9 100644
--- a/systemd/generate_test.go
+++ b/systemd/generate_test.go
@@ -49,7 +49,6 @@ After=network-online.target
 Type=notify
 WorkingDirectory=workdir
 ExecStart=commandLine
-Nice=5
 Environment="HOME=%s"
 `
 
@@ -94,7 +93,6 @@ Description=job description
 Type=notify
 WorkingDirectory=workdir
 ExecStart=commandLine
-Nice=5
 Environment="HOME=%s"
 `
 	const expectedTimer = `[Unit]
@@ -437,6 +435,131 @@ func TestGenerateOnReadOnlyFs(t *testing.T) {
 	require.Error(t, err)
 }
 
+func TestGeneratePriorityFields(t *testing.T) {
+	jobName := "name"
+	jobCommand := "backup"
+	testCases := []struct {
+		config      Config
+		contains    []string
+		notContains []string
+	}{
+		{
+			config: Config{
+				CommandLine:      "commandLine",
+				WorkingDirectory: "workdir",
+				Title:            jobName,
+				SubTitle:         jobCommand,
+				Schedules:        []string{"daily"},
+				UnitType:         SystemUnit,
+				Priority:         "",
+			},
+			contains:    []string{"ExecStart=commandLine\n"},
+			notContains: []string{"CPUSchedulingPolicy=", "Nice=", "IOSchedulingClass=", "IOSchedulingPriority="},
+		},
+		{
+			config: Config{
+				CommandLine:      "commandLine",
+				WorkingDirectory: "workdir",
+				Title:            jobName,
+				SubTitle:         jobCommand,
+				Schedules:        []string{"daily"},
+				UnitType:         SystemUnit,
+				Priority:         "standard",
+			},
+			contains:    []string{"ExecStart=commandLine\n"},
+			notContains: []string{"CPUSchedulingPolicy=", "Nice=", "IOSchedulingClass=", "IOSchedulingPriority="},
+		},
+		{
+			config: Config{
+				CommandLine:      "commandLine",
+				WorkingDirectory: "workdir",
+				Title:            jobName,
+				SubTitle:         jobCommand,
+				Schedules:        []string{"daily"},
+				UnitType:         SystemUnit,
+				Priority:         "background",
+			},
+			contains:    []string{"CPUSchedulingPolicy=idle\n"},
+			notContains: []string{"Nice=", "IOSchedulingClass=", "IOSchedulingPriority="},
+		},
+		{
+			config: Config{
+				CommandLine:      "commandLine",
+				WorkingDirectory: "workdir",
+				Title:            jobName,
+				SubTitle:         jobCommand,
+				Schedules:        []string{"daily"},
+				UnitType:         SystemUnit,
+				Priority:         "standard",
+				Nice:             10,
+			},
+			contains:    []string{"Nice=10\n"},
+			notContains: []string{"CPUSchedulingPolicy=", "IOSchedulingClass=", "IOSchedulingPriority="},
+		},
+		{
+			config: Config{
+				CommandLine:          "commandLine",
+				WorkingDirectory:     "workdir",
+				Title:                jobName,
+				SubTitle:             jobCommand,
+				Schedules:            []string{"daily"},
+				UnitType:             SystemUnit,
+				IOSchedulingClass:    2,
+				IOSchedulingPriority: 7,
+			},
+			contains:    []string{"IOSchedulingClass=2\n", "IOSchedulingPriority=7\n"},
+			notContains: []string{"CPUSchedulingPolicy=", "Nice="},
+		},
+		{
+			config: Config{
+				CommandLine:          "commandLine",
+				WorkingDirectory:     "workdir",
+				Title:                jobName,
+				SubTitle:             jobCommand,
+				Schedules:            []string{"daily"},
+				UnitType:             SystemUnit,
+				Priority:             "background",
+				Nice:                 11,
+				IOSchedulingClass:    3,
+				IOSchedulingPriority: 7,
+			},
+			contains:    []string{"CPUSchedulingPolicy=idle\n", "Nice=11\n", "IOSchedulingClass=3\n", "IOSchedulingPriority=7\n"},
+			notContains: []string{},
+		},
+	}
+
+	systemdDir := GetSystemDir()
+	baseFile := fmt.Sprintf("resticprofile-%s@profile-%s", jobCommand, jobName)
+	serviceFile := filepath.Join(systemdDir, baseFile+".service")
+
+	for _, testCase := range testCases {
+		t.Run("", func(t *testing.T) {
+			fs = afero.NewMemMapFs()
+
+			assertNoFileExists(t, serviceFile)
+
+			err := Generate(testCase.config)
+			require.NoError(t, err)
+
+			requireFileExists(t, serviceFile)
+
+			contents, err := afero.ReadFile(fs, serviceFile)
+			require.NoError(t, err)
+
+			if len(testCase.contains) > 0 {
+				for _, c := range testCase.contains {
+					assert.Contains(t, string(contents), c)
+				}
+			}
+			if len(testCase.notContains) > 0 {
+				for _, c := range testCase.notContains {
+					assert.NotContains(t, string(contents), c)
+				}
+			}
+		})
+	}
+}
+
 func assertNoFileExists(t *testing.T, filename string) {
 	t.Helper()
 	exists, err := afero.Exists(fs, filename)