Skip to content

Commit e9794b3

Browse files
committed
Add systemd user unit feature
1 parent 3d576bf commit e9794b3

File tree

14 files changed

+788
-133
lines changed

14 files changed

+788
-133
lines changed

config/shared/errors/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ var (
8686
ErrInvalidSystemdDropinExt = errors.New("invalid systemd drop-in extension")
8787
ErrNoSystemdExt = errors.New("no systemd unit extension")
8888
ErrInvalidInstantiatedUnit = errors.New("invalid systemd instantiated unit")
89+
ErrInvalidUnitScope = errors.New("unit scope must be system, user or global")
90+
ErrUnitNoUsersDefined = errors.New("when 'user' scope is used you must set at least one user")
91+
ErrUnitUsersDefined = errors.New("'users' should be specified only if scope is 'user'")
8992

9093
// Misc errors
9194
ErrSourceRequired = errors.New("source is required")

config/v3_4_experimental/schema/ignition.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,15 @@
507507
"enabled": {
508508
"type": ["boolean", "null"]
509509
},
510+
"scope": {
511+
"type": ["string", "null"]
512+
},
513+
"users": {
514+
"type": "array",
515+
"items": {
516+
"type": "string"
517+
}
518+
},
510519
"mask": {
511520
"type": ["boolean", "null"]
512521
},

config/v3_4_experimental/translate/translate.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,22 @@ func translateDirectoryEmbedded1(old old_types.DirectoryEmbedded1) (ret types.Di
5353
return
5454
}
5555

56+
func translateUnit(old old_types.Unit) (ret types.Unit) {
57+
tr := translate.NewTranslator()
58+
tr.Translate(&old.Contents, &ret.Contents)
59+
tr.Translate(&old.Dropins, &ret.Dropins)
60+
tr.Translate(&old.Enabled, &ret.Enabled)
61+
tr.Translate(&old.Mask, &ret.Mask)
62+
tr.Translate(&old.Name, &ret.Name)
63+
return
64+
}
65+
5666
func Translate(old old_types.Config) (ret types.Config) {
5767
tr := translate.NewTranslator()
5868
tr.AddCustomTranslator(translateIgnition)
5969
tr.AddCustomTranslator(translateDirectoryEmbedded1)
6070
tr.AddCustomTranslator(translateFileEmbedded1)
71+
tr.AddCustomTranslator(translateUnit)
6172
tr.Translate(&old, &ret)
6273
return
6374
}

config/v3_4_experimental/types/schema.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,13 +242,17 @@ type Timeouts struct {
242242
}
243243

244244
type Unit struct {
245-
Contents *string `json:"contents,omitempty"`
246-
Dropins []Dropin `json:"dropins,omitempty"`
247-
Enabled *bool `json:"enabled,omitempty"`
248-
Mask *bool `json:"mask,omitempty"`
249-
Name string `json:"name"`
245+
Contents *string `json:"contents,omitempty"`
246+
Dropins []Dropin `json:"dropins,omitempty"`
247+
Enabled *bool `json:"enabled,omitempty"`
248+
Mask *bool `json:"mask,omitempty"`
249+
Name string `json:"name"`
250+
Scope *string `json:"scope,omitempty"`
251+
Users []UnitUser `json:"users,omitempty"`
250252
}
251253

254+
type UnitUser string
255+
252256
type Verification struct {
253257
Hash *string `json:"hash,omitempty"`
254258
}

config/v3_4_experimental/types/unit.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ import (
2929
)
3030

3131
func (u Unit) Key() string {
32-
return u.Name
32+
if u.Scope != nil {
33+
return *u.Scope + "." + u.Name
34+
} else {
35+
return "system." + u.Name
36+
}
3337
}
3438

3539
func (d Dropin) Key() string {
@@ -43,10 +47,39 @@ func (u Unit) Validate(c cpath.ContextPath) (r report.Report) {
4347
r.AddOnError(c, err)
4448

4549
r.AddOnWarn(c, validations.ValidateInstallSection(u.Name, util.IsTrue(u.Enabled), util.NilOrEmpty(u.Contents), opts))
50+
r.AddOnError(c.Append("scope"), validateScope(u.Scope))
51+
52+
err = validateUsers(u)
53+
if err != nil && err == errors.ErrUnitUsersDefined {
54+
r.AddOnWarn(c.Append("users"), err)
55+
} else {
56+
r.AddOnError(c.Append("users"), err)
57+
}
4658

4759
return
4860
}
4961

62+
func validateScope(scope *string) error {
63+
if scope == nil {
64+
return nil
65+
}
66+
switch *scope {
67+
case "system", "user", "global":
68+
return nil
69+
default:
70+
return errors.ErrInvalidUnitScope
71+
}
72+
}
73+
74+
func validateUsers(u Unit) error {
75+
if u.Scope != nil && *u.Scope == "user" && len(u.Users) == 0 {
76+
return errors.ErrUnitNoUsersDefined
77+
} else if len(u.Users) > 0 && *u.Scope != "user" {
78+
return errors.ErrUnitUsersDefined
79+
}
80+
return nil
81+
}
82+
5083
func validateName(name string) error {
5184
switch path.Ext(name) {
5285
case ".service", ".socket", ".device", ".mount", ".automount", ".swap", ".target", ".path", ".timer", ".snapshot", ".slice", ".scope":

docs/configuration-v3_4_experimental.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ The Ignition configuration is a JSON document conforming to the following specif
156156
* **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`.
157157
* **name** (string): the name of the drop-in. This must be suffixed with ".conf".
158158
* **_contents_** (string): the contents of the drop-in.
159+
* **_scope_** (string): `system` for a system unit, `global` for a user unit applied to all users, or `user` for a user unit applied to users specified by **_users_**. Default is `system`.
160+
* **_users_** (list of strings): usernames of users to be affected by a user unit.
159161
* **_passwd_** (object): describes the desired additions to the passwd database.
160162
* **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`.
161163
* **name** (string): the username for the account.

internal/exec/stages/files/units.go

Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type Preset struct {
3333
enabled bool
3434
instantiatable bool
3535
instances []string
36+
scope util.UnitScope
3637
}
3738

3839
// warnOnOldSystemdVersion checks the version of Systemd
@@ -69,35 +70,33 @@ func (s *stage) createUnits(config types.Config) error {
6970
if err != nil {
7071
return err
7172
}
72-
key := fmt.Sprintf("%s-%s", unitName, identifier)
73+
key := fmt.Sprintf("%s.%s-%s", util.GetUnitScope(unit), unitName, identifier)
7374
if _, ok := presets[key]; ok {
7475
presets[key].instances = append(presets[key].instances, instance)
7576
} else {
76-
presets[key] = &Preset{unitName, *unit.Enabled, true, []string{instance}}
77+
presets[key] = &Preset{unitName, *unit.Enabled, true, []string{instance}, util.GetUnitScope(unit)}
7778
}
7879
} else {
79-
key := fmt.Sprintf("%s-%s", unit.Name, identifier)
80-
if _, ok := presets[unit.Name]; !ok {
81-
presets[key] = &Preset{unit.Name, *unit.Enabled, false, []string{}}
80+
key := fmt.Sprintf("%s-%s", unit.Key(), identifier)
81+
if _, ok := presets[key]; !ok {
82+
presets[key] = &Preset{unit.Name, *unit.Enabled, false, []string{}, util.GetUnitScope(unit)}
8283
} else {
8384
return fmt.Errorf("%q key is already present in the presets map", key)
8485
}
8586
}
8687
}
8788
if unit.Mask != nil {
8889
if *unit.Mask { // mask: true
89-
relabelpath := ""
9090
if err := s.Logger.LogOp(
9191
func() error {
92-
var err error
93-
relabelpath, err = s.MaskUnit(unit)
92+
var err error = s.MaskUnit(unit)
9493
return err
9594
},
96-
"masking unit %q", unit.Name,
95+
"masking unit %q for scope %q", unit.Name, string(util.GetUnitScope(unit)),
9796
); err != nil {
9897
return err
9998
}
100-
s.relabel(relabelpath)
99+
101100
} else { // mask: false
102101
masked, err := s.IsUnitMasked(unit)
103102
if err != nil {
@@ -108,7 +107,7 @@ func (s *stage) createUnits(config types.Config) error {
108107
func() error {
109108
return s.UnmaskUnit(unit)
110109
},
111-
"unmasking unit %q", unit.Name,
110+
"unmasking unit %q for scope %q", unit.Name, string(util.GetUnitScope(unit)),
112111
); err != nil {
113112
return err
114113
}
@@ -118,7 +117,7 @@ func (s *stage) createUnits(config types.Config) error {
118117
}
119118
// if we have presets then create the systemd preset file.
120119
if len(presets) != 0 {
121-
if err := s.createSystemdPresetFile(presets); err != nil {
120+
if err := s.createSystemdPresetFiles(presets); err != nil {
122121
return err
123122
}
124123
}
@@ -145,31 +144,31 @@ func parseInstanceUnit(unit types.Unit) (string, string, error) {
145144

146145
// createSystemdPresetFile creates the presetfile for enabled/disabled
147146
// systemd units.
148-
func (s *stage) createSystemdPresetFile(presets map[string]*Preset) error {
149-
if err := s.relabelPath(filepath.Join(s.DestDir, util.PresetPath)); err != nil {
150-
return err
151-
}
147+
func (s *stage) createSystemdPresetFiles(presets map[string]*Preset) error {
152148
hasInstanceUnit := false
153-
for _, value := range presets {
154-
unitString := value.unit
155-
if value.instantiatable {
149+
for _, preset := range presets {
150+
if err := s.relabelPath(filepath.Join(s.DestDir, s.SystemdPresetPath(preset.scope))); err != nil {
151+
return err
152+
}
153+
unitString := preset.unit
154+
if preset.instantiatable {
156155
hasInstanceUnit = true
157156
// Let's say we have two instantiated enabled units listed under
158157
// the systemd units i.e. [email protected], [email protected]
159158
// then the unitString will look like "[email protected] foo bar"
160-
unitString = fmt.Sprintf("%s %s", unitString, strings.Join(value.instances, " "))
159+
unitString = fmt.Sprintf("%s %s", unitString, strings.Join(preset.instances, " "))
161160
}
162-
if value.enabled {
161+
if preset.enabled {
163162
if err := s.Logger.LogOp(
164-
func() error { return s.EnableUnit(unitString) },
165-
"setting preset to enabled for %q", unitString,
163+
func() error { return s.EnableUnit(unitString, preset.scope) },
164+
"setting %q preset to enabled for %q", preset.scope, unitString,
166165
); err != nil {
167166
return err
168167
}
169168
} else {
170169
if err := s.Logger.LogOp(
171-
func() error { return s.DisableUnit(unitString) },
172-
"setting preset to disabled for %q", unitString,
170+
func() error { return s.DisableUnit(unitString, preset.scope) },
171+
"setting %q preset to disabled for %q", preset.scope, unitString,
173172
); err != nil {
174173
return err
175174
}
@@ -191,55 +190,61 @@ func (s *stage) createSystemdPresetFile(presets map[string]*Preset) error {
191190
// applies to the unit's dropins.
192191
func (s *stage) writeSystemdUnit(unit types.Unit) error {
193192
return s.Logger.LogOp(func() error {
194-
relabeledDropinDir := false
195193
for _, dropin := range unit.Dropins {
196194
if dropin.Contents == nil {
197195
continue
198196
}
199-
f, err := s.FileFromSystemdUnitDropin(unit, dropin)
197+
fetchops, err := s.FilesFromSystemdUnitDropin(unit, dropin)
200198
if err != nil {
201199
s.Logger.Crit("error converting systemd dropin: %v", err)
202200
return err
203201
}
204-
// trim off prefix since this needs to be relative to the sysroot
205-
if !strings.HasPrefix(f.Node.Path, s.DestDir) {
206-
panic(fmt.Sprintf("Dropin path %s isn't under prefix %s", f.Node.Path, s.DestDir))
207-
}
208-
relabelPath := f.Node.Path[len(s.DestDir):]
209-
if err := s.Logger.LogOp(
210-
func() error { return s.PerformFetch(f) },
211-
"writing systemd drop-in %q at %q", dropin.Name, f.Node.Path,
212-
); err != nil {
213-
return err
214-
}
215-
if !relabeledDropinDir {
216-
s.relabel(filepath.Dir(relabelPath))
217-
relabeledDropinDir = true
202+
for _, f := range fetchops {
203+
relabeledDropinDir := false
204+
// trim off prefix since this needs to be relative to the sysroot
205+
if !strings.HasPrefix(f.Node.Path, s.DestDir) {
206+
panic(fmt.Sprintf("Dropin path %s isn't under prefix %s", f.Node.Path, s.DestDir))
207+
}
208+
relabelPath := f.Node.Path[len(s.DestDir):]
209+
if err := s.Logger.LogOp(
210+
func() error { return s.PerformFetch(f) },
211+
"writing systemd drop-in %q at %q", dropin.Name, f.Node.Path,
212+
); err != nil {
213+
return err
214+
}
215+
if !relabeledDropinDir {
216+
s.relabel(filepath.Dir(relabelPath))
217+
relabeledDropinDir = true
218+
}
218219
}
219220
}
220221

221222
if cutil.NilOrEmpty(unit.Contents) {
222223
return nil
223224
}
224225

225-
f, err := s.FileFromSystemdUnit(unit)
226+
fetchops, err := s.FilesFromSystemdUnit(unit)
226227
if err != nil {
227228
s.Logger.Crit("error converting unit: %v", err)
228229
return err
229230
}
230-
// trim off prefix since this needs to be relative to the sysroot
231-
if !strings.HasPrefix(f.Node.Path, s.DestDir) {
232-
panic(fmt.Sprintf("Unit path %s isn't under prefix %s", f.Node.Path, s.DestDir))
233-
}
234-
relabelPath := f.Node.Path[len(s.DestDir):]
235-
if err := s.Logger.LogOp(
236-
func() error { return s.PerformFetch(f) },
237-
"writing unit %q at %q", unit.Name, f.Node.Path,
238-
); err != nil {
239-
return err
231+
232+
for _, f := range fetchops {
233+
// trim off prefix since this needs to be relative to the sysroot
234+
if !strings.HasPrefix(f.Node.Path, s.DestDir) {
235+
panic(fmt.Sprintf("Unit path %s isn't under prefix %s", f.Node.Path, s.DestDir))
236+
}
237+
relabelPath := f.Node.Path[len(s.DestDir):]
238+
if err := s.Logger.LogOp(
239+
func() error { return s.PerformFetch(f) },
240+
"writing unit %q at %q", unit.Name, f.Node.Path,
241+
); err != nil {
242+
return err
243+
}
244+
245+
s.relabel(relabelPath)
240246
}
241-
s.relabel(relabelPath)
242247

243248
return nil
244-
}, "processing unit %q", unit.Name)
249+
}, "processing unit %q for scope %q", unit.Name, string(util.GetUnitScope(unit)))
245250
}

0 commit comments

Comments
 (0)