Skip to content

Commit

Permalink
Add systemd user unit feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Nemric committed Mar 31, 2022
1 parent 53fad9f commit 0ffb933
Show file tree
Hide file tree
Showing 14 changed files with 788 additions and 133 deletions.
3 changes: 3 additions & 0 deletions config/shared/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ var (
ErrInvalidSystemdDropinExt = errors.New("invalid systemd drop-in extension")
ErrNoSystemdExt = errors.New("no systemd unit extension")
ErrInvalidInstantiatedUnit = errors.New("invalid systemd instantiated unit")
ErrInvalidUnitScope = errors.New("unit scope must be system, user or global")
ErrUnitNoUsersDefined = errors.New("when 'user' scope is used you must set at least one user")
ErrUnitUsersDefined = errors.New("'users' should be specified only if scope is 'user'")

// Misc errors
ErrSourceRequired = errors.New("source is required")
Expand Down
9 changes: 9 additions & 0 deletions config/v3_4_experimental/schema/ignition.json
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,15 @@
"enabled": {
"type": ["boolean", "null"]
},
"scope": {
"type": ["string", "null"]
},
"users": {
"type": "array",
"items": {
"type": "string"
}
},
"mask": {
"type": ["boolean", "null"]
},
Expand Down
11 changes: 11 additions & 0 deletions config/v3_4_experimental/translate/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,20 @@ func translateIgnition(old old_types.Ignition) (ret types.Ignition) {
return
}

func translateUnit(old old_types.Unit) (ret types.Unit) {
tr := translate.NewTranslator()
tr.Translate(&old.Contents, &ret.Contents)
tr.Translate(&old.Dropins, &ret.Dropins)
tr.Translate(&old.Enabled, &ret.Enabled)
tr.Translate(&old.Mask, &ret.Mask)
tr.Translate(&old.Name, &ret.Name)
return
}

func Translate(old old_types.Config) (ret types.Config) {
tr := translate.NewTranslator()
tr.AddCustomTranslator(translateIgnition)
tr.AddCustomTranslator(translateUnit)
tr.Translate(&old, &ret)
return
}
14 changes: 9 additions & 5 deletions config/v3_4_experimental/types/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,17 @@ type Timeouts struct {
}

type Unit struct {
Contents *string `json:"contents,omitempty"`
Dropins []Dropin `json:"dropins,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Mask *bool `json:"mask,omitempty"`
Name string `json:"name"`
Contents *string `json:"contents,omitempty"`
Dropins []Dropin `json:"dropins,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Mask *bool `json:"mask,omitempty"`
Name string `json:"name"`
Scope *string `json:"scope,omitempty"`
Users []UnitUser `json:"users,omitempty"`
}

type UnitUser string

type Verification struct {
Hash *string `json:"hash,omitempty"`
}
35 changes: 34 additions & 1 deletion config/v3_4_experimental/types/unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ import (
)

func (u Unit) Key() string {
return u.Name
if u.Scope != nil {
return *u.Scope + "." + u.Name
} else {
return "system." + u.Name
}
}

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

r.AddOnWarn(c, validations.ValidateInstallSection(u.Name, util.IsTrue(u.Enabled), util.NilOrEmpty(u.Contents), opts))
r.AddOnError(c.Append("scope"), validateScope(u.Scope))

err = validateUsers(u)
if err != nil && err == errors.ErrUnitUsersDefined {
r.AddOnWarn(c.Append("users"), err)
} else {
r.AddOnError(c.Append("users"), err)
}

return
}

func validateScope(scope *string) error {
if scope == nil {
return nil
}
switch *scope {
case "system", "user", "global":
return nil
default:
return errors.ErrInvalidUnitScope
}
}

func validateUsers(u Unit) error {
if u.Scope != nil && *u.Scope == "user" && len(u.Users) == 0 {
return errors.ErrUnitNoUsersDefined
} else if len(u.Users) > 0 && *u.Scope != "user" {
return errors.ErrUnitUsersDefined
}
return nil
}

func validateName(name string) error {
switch path.Ext(name) {
case ".service", ".socket", ".device", ".mount", ".automount", ".swap", ".target", ".path", ".timer", ".snapshot", ".slice", ".scope":
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration-v3_4_experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ The Ignition configuration is a JSON document conforming to the following specif
* **_dropins_** (list of objects): the list of drop-ins for the unit. Every drop-in must have a unique `name`.
* **name** (string): the name of the drop-in. This must be suffixed with ".conf".
* **_contents_** (string): the contents of the drop-in.
* **_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`.
* **_users_** (list of strings): usernames of users to be affected by a user unit.
* **_passwd_** (object): describes the desired additions to the passwd database.
* **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`.
* **name** (string): the username for the account.
Expand Down
113 changes: 59 additions & 54 deletions internal/exec/stages/files/units.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Preset struct {
enabled bool
instantiatable bool
instances []string
scope util.UnitScope
}

// warnOnOldSystemdVersion checks the version of Systemd
Expand Down Expand Up @@ -69,35 +70,33 @@ func (s *stage) createUnits(config types.Config) error {
if err != nil {
return err
}
key := fmt.Sprintf("%s-%s", unitName, identifier)
key := fmt.Sprintf("%s.%s-%s", util.GetUnitScope(unit), unitName, identifier)
if _, ok := presets[key]; ok {
presets[key].instances = append(presets[key].instances, instance)
} else {
presets[key] = &Preset{unitName, *unit.Enabled, true, []string{instance}}
presets[key] = &Preset{unitName, *unit.Enabled, true, []string{instance}, util.GetUnitScope(unit)}
}
} else {
key := fmt.Sprintf("%s-%s", unit.Name, identifier)
if _, ok := presets[unit.Name]; !ok {
presets[key] = &Preset{unit.Name, *unit.Enabled, false, []string{}}
key := fmt.Sprintf("%s-%s", unit.Key(), identifier)
if _, ok := presets[key]; !ok {
presets[key] = &Preset{unit.Name, *unit.Enabled, false, []string{}, util.GetUnitScope(unit)}
} else {
return fmt.Errorf("%q key is already present in the presets map", key)
}
}
}
if unit.Mask != nil {
if *unit.Mask { // mask: true
relabelpath := ""
if err := s.Logger.LogOp(
func() error {
var err error
relabelpath, err = s.MaskUnit(unit)
var err error = s.MaskUnit(unit)
return err
},
"masking unit %q", unit.Name,
"masking unit %q for scope %q", unit.Name, string(util.GetUnitScope(unit)),
); err != nil {
return err
}
s.relabel(relabelpath)

} else { // mask: false
masked, err := s.IsUnitMasked(unit)
if err != nil {
Expand All @@ -108,7 +107,7 @@ func (s *stage) createUnits(config types.Config) error {
func() error {
return s.UnmaskUnit(unit)
},
"unmasking unit %q", unit.Name,
"unmasking unit %q for scope %q", unit.Name, string(util.GetUnitScope(unit)),
); err != nil {
return err
}
Expand All @@ -118,7 +117,7 @@ func (s *stage) createUnits(config types.Config) error {
}
// if we have presets then create the systemd preset file.
if len(presets) != 0 {
if err := s.createSystemdPresetFile(presets); err != nil {
if err := s.createSystemdPresetFiles(presets); err != nil {
return err
}
}
Expand All @@ -145,31 +144,31 @@ func parseInstanceUnit(unit types.Unit) (string, string, error) {

// createSystemdPresetFile creates the presetfile for enabled/disabled
// systemd units.
func (s *stage) createSystemdPresetFile(presets map[string]*Preset) error {
if err := s.relabelPath(filepath.Join(s.DestDir, util.PresetPath)); err != nil {
return err
}
func (s *stage) createSystemdPresetFiles(presets map[string]*Preset) error {
hasInstanceUnit := false
for _, value := range presets {
unitString := value.unit
if value.instantiatable {
for _, preset := range presets {
if err := s.relabelPath(filepath.Join(s.DestDir, s.SystemdPresetPath(preset.scope))); err != nil {
return err
}
unitString := preset.unit
if preset.instantiatable {
hasInstanceUnit = true
// Let's say we have two instantiated enabled units listed under
// the systemd units i.e. echo@foo.service, echo@bar.service
// then the unitString will look like "echo@.service foo bar"
unitString = fmt.Sprintf("%s %s", unitString, strings.Join(value.instances, " "))
unitString = fmt.Sprintf("%s %s", unitString, strings.Join(preset.instances, " "))
}
if value.enabled {
if preset.enabled {
if err := s.Logger.LogOp(
func() error { return s.EnableUnit(unitString) },
"setting preset to enabled for %q", unitString,
func() error { return s.EnableUnit(unitString, preset.scope) },
"setting %q preset to enabled for %q", preset.scope, unitString,
); err != nil {
return err
}
} else {
if err := s.Logger.LogOp(
func() error { return s.DisableUnit(unitString) },
"setting preset to disabled for %q", unitString,
func() error { return s.DisableUnit(unitString, preset.scope) },
"setting %q preset to disabled for %q", preset.scope, unitString,
); err != nil {
return err
}
Expand All @@ -191,55 +190,61 @@ func (s *stage) createSystemdPresetFile(presets map[string]*Preset) error {
// applies to the unit's dropins.
func (s *stage) writeSystemdUnit(unit types.Unit) error {
return s.Logger.LogOp(func() error {
relabeledDropinDir := false
for _, dropin := range unit.Dropins {
if dropin.Contents == nil {
continue
}
f, err := s.FileFromSystemdUnitDropin(unit, dropin)
fetchops, err := s.FilesFromSystemdUnitDropin(unit, dropin)
if err != nil {
s.Logger.Crit("error converting systemd dropin: %v", err)
return err
}
// trim off prefix since this needs to be relative to the sysroot
if !strings.HasPrefix(f.Node.Path, s.DestDir) {
panic(fmt.Sprintf("Dropin path %s isn't under prefix %s", f.Node.Path, s.DestDir))
}
relabelPath := f.Node.Path[len(s.DestDir):]
if err := s.Logger.LogOp(
func() error { return s.PerformFetch(f) },
"writing systemd drop-in %q at %q", dropin.Name, f.Node.Path,
); err != nil {
return err
}
if !relabeledDropinDir {
s.relabel(filepath.Dir(relabelPath))
relabeledDropinDir = true
for _, f := range fetchops {
relabeledDropinDir := false
// trim off prefix since this needs to be relative to the sysroot
if !strings.HasPrefix(f.Node.Path, s.DestDir) {
panic(fmt.Sprintf("Dropin path %s isn't under prefix %s", f.Node.Path, s.DestDir))
}
relabelPath := f.Node.Path[len(s.DestDir):]
if err := s.Logger.LogOp(
func() error { return s.PerformFetch(f) },
"writing systemd drop-in %q at %q", dropin.Name, f.Node.Path,
); err != nil {
return err
}
if !relabeledDropinDir {
s.relabel(filepath.Dir(relabelPath))
relabeledDropinDir = true
}
}
}

if cutil.NilOrEmpty(unit.Contents) {
return nil
}

f, err := s.FileFromSystemdUnit(unit)
fetchops, err := s.FilesFromSystemdUnit(unit)
if err != nil {
s.Logger.Crit("error converting unit: %v", err)
return err
}
// trim off prefix since this needs to be relative to the sysroot
if !strings.HasPrefix(f.Node.Path, s.DestDir) {
panic(fmt.Sprintf("Unit path %s isn't under prefix %s", f.Node.Path, s.DestDir))
}
relabelPath := f.Node.Path[len(s.DestDir):]
if err := s.Logger.LogOp(
func() error { return s.PerformFetch(f) },
"writing unit %q at %q", unit.Name, f.Node.Path,
); err != nil {
return err

for _, f := range fetchops {
// trim off prefix since this needs to be relative to the sysroot
if !strings.HasPrefix(f.Node.Path, s.DestDir) {
panic(fmt.Sprintf("Unit path %s isn't under prefix %s", f.Node.Path, s.DestDir))
}
relabelPath := f.Node.Path[len(s.DestDir):]
if err := s.Logger.LogOp(
func() error { return s.PerformFetch(f) },
"writing unit %q at %q", unit.Name, f.Node.Path,
); err != nil {
return err
}

s.relabel(relabelPath)
}
s.relabel(relabelPath)

return nil
}, "processing unit %q", unit.Name)
}, "processing unit %q for scope %q", unit.Name, string(util.GetUnitScope(unit)))
}
Loading

0 comments on commit 0ffb933

Please sign in to comment.