From 75562396ccb8c7a8f5db252996d30ffef3801df7 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Thu, 3 Feb 2022 18:03:59 -0500 Subject: [PATCH 1/2] files: fix duplicate relabeling of preset file The relabelPath() in createUnits() is the correct call, since it also handles the parent directory. Move it into createSystemdPresetFile() for clarity and delete the other one. Fixes: 02896114c9 ("files: relabel the systemd preset directory") --- internal/exec/stages/files/units.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/exec/stages/files/units.go b/internal/exec/stages/files/units.go index f6f720ca43..f7ded9e895 100644 --- a/internal/exec/stages/files/units.go +++ b/internal/exec/stages/files/units.go @@ -118,9 +118,6 @@ 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.relabelPath(filepath.Join(s.DestDir, util.PresetPath)); err != nil { - return err - } if err := s.createSystemdPresetFile(presets); err != nil { return err } @@ -149,6 +146,9 @@ 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 + } hasInstanceUnit := false for _, value := range presets { unitString := value.unit @@ -183,7 +183,6 @@ func (s *stage) createSystemdPresetFile(presets map[string]*Preset) error { return err } } - s.relabel(util.PresetPath) return nil } From 987794fb9832a7e28d5229b82cfd7bd1d6dafed6 Mon Sep 17 00:00:00 2001 From: Nemric <56299157+Nemric@users.noreply.github.com> Date: Thu, 10 Feb 2022 15:36:51 +0000 Subject: [PATCH 2/2] Add systemd user unit feature --- config/shared/errors/errors.go | 2 + config/v3_4_experimental/schema/ignition.json | 9 + .../v3_4_experimental/translate/translate.go | 11 + config/v3_4_experimental/types/schema.go | 14 +- config/v3_4_experimental/types/unit.go | 29 ++- docs/configuration-v3_4_experimental.md | 2 + internal/exec/stages/files/units.go | 119 +++++---- internal/exec/stages/files/units_test.go | 246 ++++++++++++++++++ internal/exec/util/passwd.go | 8 + internal/exec/util/path.go | 44 +++- internal/exec/util/unit.go | 178 +++++++------ tests/positive/systemd/create_unit.go | 90 +++++++ 12 files changed, 613 insertions(+), 139 deletions(-) mode change 100644 => 100755 internal/exec/util/path.go mode change 100644 => 100755 internal/exec/util/unit.go diff --git a/config/shared/errors/errors.go b/config/shared/errors/errors.go index 7761280d07..b4db06f54f 100644 --- a/config/shared/errors/errors.go +++ b/config/shared/errors/errors.go @@ -85,6 +85,8 @@ 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") + ErrNoUserDefined = errors.New("when 'user' scope is used you must set at least one user") // Misc errors ErrSourceRequired = errors.New("source is required") diff --git a/config/v3_4_experimental/schema/ignition.json b/config/v3_4_experimental/schema/ignition.json index d93ce6c2d0..f0499db934 100644 --- a/config/v3_4_experimental/schema/ignition.json +++ b/config/v3_4_experimental/schema/ignition.json @@ -507,6 +507,15 @@ "enabled": { "type": ["boolean", "null"] }, + "scope": { + "type": ["string", "null"] + }, + "users": { + "type": "array", + "items": { + "type": "string" + } + }, "mask": { "type": ["boolean", "null"] }, diff --git a/config/v3_4_experimental/translate/translate.go b/config/v3_4_experimental/translate/translate.go index 5b39cae9b8..3b6deb9ae2 100644 --- a/config/v3_4_experimental/translate/translate.go +++ b/config/v3_4_experimental/translate/translate.go @@ -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 } diff --git a/config/v3_4_experimental/types/schema.go b/config/v3_4_experimental/types/schema.go index ca25b99ea7..e75e3880db 100644 --- a/config/v3_4_experimental/types/schema.go +++ b/config/v3_4_experimental/types/schema.go @@ -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"` } diff --git a/config/v3_4_experimental/types/unit.go b/config/v3_4_experimental/types/unit.go index bc2d3299c4..9c6457ca5a 100644 --- a/config/v3_4_experimental/types/unit.go +++ b/config/v3_4_experimental/types/unit.go @@ -29,7 +29,13 @@ import ( ) func (u Unit) Key() string { - return u.Name + // Change unit Key to differenciate them by concatenating their scope and name + // to handle the case of same named units for differents scope + if u.Scope != nil { + return *u.Scope + "." + u.Name + } else { + return "system." + u.Name + } } func (d Dropin) Key() string { @@ -43,10 +49,31 @@ 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)) + r.AddOnError(c, validateUsers(u)) 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" && u.Users == nil { + return errors.ErrNoUserDefined + } + return nil +} + func validateName(name string) error { switch path.Ext(name) { case ".service", ".socket", ".device", ".mount", ".automount", ".swap", ".target", ".path", ".timer", ".snapshot", ".slice", ".scope": diff --git a/docs/configuration-v3_4_experimental.md b/docs/configuration-v3_4_experimental.md index 1520e9605b..020ca53d3b 100644 --- a/docs/configuration-v3_4_experimental.md +++ b/docs/configuration-v3_4_experimental.md @@ -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): Allow to define "user", "global" or "system" (default) level systemd units. cf : [systemd Documentation](https://www.freedesktop.org/software/systemd/man/systemctl.html#enable%20UNIT%E2%80%A6) + * **_users_** (list of strings): The list of users concerned by the unit if scope is set to "user". * **_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. diff --git a/internal/exec/stages/files/units.go b/internal/exec/stages/files/units.go index f7ded9e895..cc8e96d27e 100644 --- a/internal/exec/stages/files/units.go +++ b/internal/exec/stages/files/units.go @@ -33,6 +33,7 @@ type Preset struct { enabled bool instantiatable bool instances []string + scope util.UnitScope } // warnOnOldSystemdVersion checks the version of Systemd @@ -69,16 +70,17 @@ 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) + // key := fmt.Sprintf("%s-%s", unit.Key(), 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) } @@ -86,18 +88,17 @@ func (s *stage) createUnits(config types.Config) error { } if unit.Mask != nil { if *unit.Mask { // mask: true - relabelpath := "" + // relabelpaths := []string{} 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", unit.Key(), ); err != nil { return err } - s.relabel(relabelpath) + } else { // mask: false masked, err := s.IsUnitMasked(unit) if err != nil { @@ -108,7 +109,7 @@ func (s *stage) createUnits(config types.Config) error { func() error { return s.UnmaskUnit(unit) }, - "unmasking unit %q", unit.Name, + "unmasking unit %q", unit.Key(), ); err != nil { return err } @@ -118,7 +119,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 } } @@ -145,30 +146,40 @@ 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 { + + //getting directories from presets file for relabling + paths := make(map[string]bool) + for _, preset := range presets { + path := filepath.Dir(s.SystemdPresetPath(preset.scope)) + if _, value := paths[path]; !value { + paths[path] = false + if err := s.relabelPath(filepath.Join(s.DestDir, path)); err != nil { + return err + } + paths[path] = true + } } hasInstanceUnit := false - for _, value := range presets { - unitString := value.unit - if value.instantiatable { + for _, preset := range presets { + 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) }, + func() error { return s.EnableUnit(unitString, preset.scope) }, "setting preset to enabled for %q", unitString, ); err != nil { return err } } else { if err := s.Logger.LogOp( - func() error { return s.DisableUnit(unitString) }, + func() error { return s.DisableUnit(unitString, preset.scope) }, "setting preset to disabled for %q", unitString, ); err != nil { return err @@ -191,30 +202,32 @@ 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 + } } } @@ -222,24 +235,28 @@ func (s *stage) writeSystemdUnit(unit types.Unit) error { 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.Key(), f.Node.Path, + ); err != nil { + return err + } + + s.relabel(relabelPath) } - s.relabel(relabelPath) return nil - }, "processing unit %q", unit.Name) + }, "processing unit %q", unit.Key()) } diff --git a/internal/exec/stages/files/units_test.go b/internal/exec/stages/files/units_test.go index b7020f7efc..b65d6cccbf 100644 --- a/internal/exec/stages/files/units_test.go +++ b/internal/exec/stages/files/units_test.go @@ -15,11 +15,20 @@ package files import ( + "fmt" + "io/ioutil" + "os" + "os/user" + "path/filepath" "reflect" "testing" "github.com/coreos/ignition/v2/config/shared/errors" + cfgutil "github.com/coreos/ignition/v2/config/util" + "github.com/coreos/ignition/v2/config/v3_4_experimental" "github.com/coreos/ignition/v2/config/v3_4_experimental/types" + "github.com/coreos/ignition/v2/internal/exec/util" + "github.com/coreos/ignition/v2/internal/log" ) func TestParseInstanceUnit(t *testing.T) { @@ -74,3 +83,240 @@ func TestParseInstanceUnit(t *testing.T) { } } } + +func TestSystemdUnitPath(t *testing.T) { + + var logg log.Logger = log.New(true) + var st stage + + st.DestDir = "/" + st.Logger = &logg + + tests := []struct { + in types.Unit + out []string + }{ + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("system")}, + []string{"etc/systemd/system"}, + }, + { + types.Unit{Name: "test.service"}, + []string{"etc/systemd/system"}, + }, + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("global")}, + []string{"etc/systemd/user"}, + }, + } + + for i, test := range tests { + path := st.SystemdUnitPaths(test.in) + if path[len(path)-1] != test.out[len(test.out)-1] { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, path) + } + } +} + +func TestSystemdDropinsPaths(t *testing.T) { + + var logg log.Logger = log.New(true) + var st stage + + st.DestDir = "/" + st.Logger = &logg + + tests := []struct { + in types.Unit + out []string + }{ + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("system")}, + []string{"etc/systemd/system/test.service.d"}, + }, + { + types.Unit{Name: "test.service"}, + []string{"etc/systemd/system/test.service.d"}, + }, + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("global")}, + []string{"etc/systemd/user/test.service.d"}, + }, + } + + for i, test := range tests { + path := st.SystemdDropinsPaths(test.in) + if path[len(path)-1] != test.out[len(test.out)-1] { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, path) + } + } +} + +func TestSystemdPresetPath(t *testing.T) { + + var logg log.Logger = log.New(true) + var st stage + + st.DestDir = "/" + st.Logger = &logg + + tests := []struct { + in types.Unit + out string + }{ + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("system")}, + "etc/systemd/system-preset/20-ignition.preset", + }, + { + types.Unit{Name: "test.service"}, + "etc/systemd/system-preset/20-ignition.preset", + }, + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("user")}, + "etc/systemd/user-preset/21-ignition.preset", + }, + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("global")}, + "etc/systemd/user-preset/20-ignition.preset", + }, + } + + for i, test := range tests { + path := st.SystemdPresetPath(util.GetUnitScope(test.in)) + if path != test.out { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, path) + } + } +} + +func TestCreateUnits(t *testing.T) { + + if os.Geteuid() != 0 { + t.Skip("test requires root for chroot(), skipping") + } + + _, err := user.Lookup("root") + if err != nil { + t.Fatalf("user lookup failed (libnss_files.so might not be loaded): %v", err) + } + + tmpdir, err := tempBase() + if err != nil { + t.Fatalf("temp base error: %v", err) + } + + var logg log.Logger = log.New(true) + var st stage + + st.checkRelabeling() + st.DestDir = tmpdir + st.Logger = &logg + + defer os.RemoveAll(tmpdir) + defer st.Logger.Close() + + var conf string = `{ + "ignition": { + "version": "3.4.0-experimental" + }, + "systemd": { + "units": [ + { + "contents": "[Unit]\nDescription=Prometheus node exporter\n[Install]\nWantedBy=multi-user.target\n", + "enabled": true, + "name": "unit1.service", + "dropins": [{ + "name": "debug.conf", + "contents": "[Service]\nEnvironment=SYSTEMD_LOG_LEVEL=debug" + }] + }, + { + "contents": "[Unit]\nDescription=promtail.service\n[Install]\nWantedBy=multi-user.target default.target", + "enabled": true, + "name": "unit1.service", + "scope": "user", + "users" : ["tester1", "tester2"], + "dropins": [{ + "name": "debug.conf", + "contents": "[Service]\nEnvironment=SYSTEMD_LOG_LEVEL=debug" + }] + }, + { + "contents": "[Unit]\nDescription=promtail.service\n[Install]\nWantedBy=multi-user.target default.target", + "enabled": true, + "name": "unit2.service", + "scope": "system" + }, + { + "contents": "[Unit]\nDescription=promtail.service\n[Install]\nWantedBy=multi-user.target default.target", + "enabled": true, + "name": "unit2.service", + "scope": "global" + }, + { + "contents": "[Unit]\nDescription=promtail.service\n[Install]\nWantedBy=multi-user.target default.target", + "enabled": true, + "name": "unit3.service", + "scope": "global", + "mask": true + }, + { + "contents": "[Unit]\nDescription=promtail.service\n[Install]\nWantedBy=multi-user.target default.target", + "enabled": false, + "name": "unit4.service", + "scope": "user", + "users" : ["tester1", "tester2"], + "mask" : true, + "dropins": [{ + "name": "debug.conf", + "contents": "[Service]\nEnvironment=SYSTEMD_LOG_LEVEL=debug" + }] + } + ] + } + }` + + config, report, err := v3_4_experimental.Parse([]byte(conf)) + + if err != nil { + fmt.Printf("error %v : \n%+v", err.Error(), report) + t.FailNow() + } + print(config.Ignition.Version) + err = st.createUnits(config) + if err != nil { + t.Errorf("error occured: %v", err) + } +} + +func tempBase() (string, error) { + td, err := ioutil.TempDir("", "igntests") + if err != nil { + return "", err + } + + if err := os.MkdirAll(filepath.Join(td, "etc"), 0755); err != nil { + return "", err + } + + gp := filepath.Join(td, "etc/group") + err = ioutil.WriteFile(gp, []byte("foo:x:4242:\n"), 0644) + if err != nil { + return "", err + } + + pp := filepath.Join(td, "etc/passwd") + err = ioutil.WriteFile(pp, []byte("tester1:x:44:4242::/home/tester1:/bin/false\ntester2:x:45:4242::/home/tester2:/bin/false"), 0644) + if err != nil { + return "", err + } + + nsp := filepath.Join(td, "etc/nsswitch.conf") + err = ioutil.WriteFile(nsp, []byte("passwd: files\ngroup: files\nshadow: files\ngshadow: files\n"), 0644) + if err != nil { + return "", err + } + + return td, nil +} diff --git a/internal/exec/util/passwd.go b/internal/exec/util/passwd.go index 267ed86147..6f5930f670 100644 --- a/internal/exec/util/passwd.go +++ b/internal/exec/util/passwd.go @@ -142,6 +142,14 @@ func (u Util) GetUserHomeDir(c types.PasswdUser) (string, error) { return usr.HomeDir, nil } +func (u Util) GetUserHomeDirByName(name string) (string, error) { + usr, err := u.userLookup(name) + if err != nil { + return "", err + } + return usr.HomeDir, nil +} + // CheckIfUserExists will return Info log when user is empty func (u Util) CheckIfUserExists(c types.PasswdUser) (bool, error) { _, err := u.userLookup(c.Name) diff --git a/internal/exec/util/path.go b/internal/exec/util/path.go old mode 100644 new mode 100755 index 73f96f84d4..d8751ec671 --- a/internal/exec/util/path.go +++ b/internal/exec/util/path.go @@ -16,12 +16,48 @@ package util import ( "path/filepath" + + "github.com/coreos/ignition/v2/config/v3_4_experimental/types" ) -func SystemdUnitsPath() string { - return filepath.Join("etc", "systemd", "system") +func (u Util) SystemdUnitPaths(unit types.Unit) []string { + var paths []string + switch GetUnitScope(unit) { + case UserUnit: + for _, user := range unit.Users { + home, err := u.GetUserHomeDirByName(string(user)) + if err != nil { + print(home, err) + } + paths = append(paths, filepath.Join(home, ".config", "systemd", "user")) + } + case SystemUnit: + paths = append(paths, filepath.Join("etc", "systemd", "system")) + case GlobalUnit: + paths = append(paths, filepath.Join("etc", "systemd", "user")) + default: + paths = append(paths, filepath.Join("etc", "systemd", "system")) + } + return paths +} + +func (u Util) SystemdPresetPath(scope UnitScope) string { + switch scope { + case UserUnit: + return filepath.Join("etc", "systemd", "user-preset", "21-ignition.preset") + case SystemUnit: + return filepath.Join("etc", "systemd", "system-preset", "20-ignition.preset") + case GlobalUnit: + return filepath.Join("etc", "systemd", "user-preset", "20-ignition.preset") + default: + return filepath.Join("etc", "systemd", "system-preset", "20-ignition.preset") + } } -func SystemdDropinsPath(unitName string) string { - return filepath.Join("etc", "systemd", "system", unitName+".d") +func (u Util) SystemdDropinsPaths(unit types.Unit) []string { + var paths []string + for _, path := range u.SystemdUnitPaths(unit) { + paths = append(paths, filepath.Join(path, unit.Name+".d")) + } + return paths } diff --git a/internal/exec/util/unit.go b/internal/exec/util/unit.go old mode 100644 new mode 100755 index 01321cfa86..25b1e6b01a --- a/internal/exec/util/unit.go +++ b/internal/exec/util/unit.go @@ -18,7 +18,6 @@ import ( "fmt" "net/url" "os" - "path/filepath" "syscall" "github.com/coreos/ignition/v2/config/v3_4_experimental/types" @@ -27,135 +26,158 @@ import ( ) const ( - PresetPath string = "/etc/systemd/system-preset/20-ignition.preset" DefaultPresetPermissions os.FileMode = 0644 ) -func (ut Util) FileFromSystemdUnit(unit types.Unit) (FetchOp, error) { +type UnitScope string + +const ( + SystemUnit UnitScope = "system" + UserUnit UnitScope = "user" + GlobalUnit UnitScope = "global" +) + +func GetUnitScope(unit types.Unit) UnitScope { + if unit.Scope == nil { + return SystemUnit + } else { + return UnitScope(*unit.Scope) + } +} + +func (ut Util) FilesFromSystemdUnit(unit types.Unit) ([]FetchOp, error) { + + var fetchops []FetchOp + if unit.Contents == nil { empty := "" unit.Contents = &empty } u, err := url.Parse(dataurl.EncodeBytes([]byte(*unit.Contents))) if err != nil { - return FetchOp{}, err + return []FetchOp{}, err } - path, err := ut.JoinPath(SystemdUnitsPath(), unit.Name) - if err != nil { - return FetchOp{}, err + for _, path := range ut.SystemdUnitPaths(unit) { + fpath, err := ut.JoinPath(path, unit.Name) + + if err != nil { + return []FetchOp{}, err + } + + fetchops = append(fetchops, FetchOp{Node: types.Node{Path: fpath}, Url: *u}) } - return FetchOp{ - Node: types.Node{ - Path: path, - }, - Url: *u, - }, nil + return fetchops, nil } -func (ut Util) FileFromSystemdUnitDropin(unit types.Unit, dropin types.Dropin) (FetchOp, error) { +func (ut Util) FilesFromSystemdUnitDropin(unit types.Unit, dropin types.Dropin) ([]FetchOp, error) { + + var fetchops []FetchOp + if dropin.Contents == nil { empty := "" dropin.Contents = &empty } + u, err := url.Parse(dataurl.EncodeBytes([]byte(*dropin.Contents))) if err != nil { - return FetchOp{}, err + return []FetchOp{}, err } - path, err := ut.JoinPath(SystemdDropinsPath(string(unit.Name)), dropin.Name) - if err != nil { - return FetchOp{}, err + for _, path := range ut.SystemdDropinsPaths(unit) { + fpath, err := ut.JoinPath(path, dropin.Name) + if err != nil { + return []FetchOp{}, err + } + fetchops = append(fetchops, FetchOp{Node: types.Node{Path: fpath}, Url: *u}) } - return FetchOp{ - Node: types.Node{ - Path: path, - }, - Url: *u, - }, nil + return fetchops, nil } -// MaskUnit writes a symlink to /dev/null to mask the specified unit and returns the path of that unit -// without the sysroot prefix -func (ut Util) MaskUnit(unit types.Unit) (string, error) { - path, err := ut.JoinPath(SystemdUnitsPath(), unit.Name) - if err != nil { - return "", err - } +// MaskUnit writes a symlink to /dev/null to mask the specified unit +func (ut Util) MaskUnit(unit types.Unit) error { + for _, path := range ut.SystemdUnitPaths(unit) { + unitpath, err := ut.JoinPath(path, unit.Name) + if err != nil { + return err + } - if err := MkdirForFile(path); err != nil { - return "", err - } - if err := os.RemoveAll(path); err != nil { - return "", err - } - if err := os.Symlink("/dev/null", path); err != nil { - return "", err + if err := MkdirForFile(unitpath); err != nil { + return err + } + if err := os.RemoveAll(unitpath); err != nil { + return err + } + if err := os.Symlink("/dev/null", unitpath); err != nil { + return err + } } - // not the same as the path above, since this lacks the sysroot prefix - return filepath.Join("/", SystemdUnitsPath(), unit.Name), nil + return nil } // UnmaskUnit deletes the symlink to /dev/null for a masked unit func (ut Util) UnmaskUnit(unit types.Unit) error { - path, err := ut.JoinPath(SystemdUnitsPath(), unit.Name) - if err != nil { - return err - } - // Make a final check to make sure the unit is masked - masked, err := ut.IsUnitMasked(unit) - if err != nil { - return err - } - // If masked, remove the symlink - if masked { - if err = os.Remove(path); err != nil { + for _, path := range ut.SystemdUnitPaths(unit) { + unitpath, err := ut.JoinPath(path, unit.Name) + if err != nil { + return err + } + // Make a final check to make sure the unit is masked + masked, err := ut.IsUnitMasked(unit) + if err != nil { return err } + // If masked, remove the symlink + if masked { + if err = os.Remove(unitpath); err != nil { + return err + } + } } return nil } // IsUnitMasked returns true/false if a systemd unit is masked func (ut Util) IsUnitMasked(unit types.Unit) (bool, error) { - path, err := ut.JoinPath(SystemdUnitsPath(), unit.Name) - if err != nil { - return false, err - } - - target, err := os.Readlink(path) - if err != nil { - if os.IsNotExist(err) { - // The path doesn't exist, hence the unit isn't masked - return false, nil - } else if e, ok := err.(*os.PathError); ok && e.Err == syscall.EINVAL { - // The path isn't a symlink, hence the unit isn't masked - return false, nil - } else { + for _, path := range ut.SystemdUnitPaths(unit) { + unitpath, err := ut.JoinPath(path, unit.Name) + if err != nil { return false, err } - } - if target != "/dev/null" { - // The symlink doesn't point to /dev/null, hence the unit isn't masked - return false, nil - } + target, err := os.Readlink(unitpath) + if err != nil { + if os.IsNotExist(err) { + // The path doesn't exist, hence the unit isn't masked + return false, nil + } else if e, ok := err.(*os.PathError); ok && e.Err == syscall.EINVAL { + // The path isn't a symlink, hence the unit isn't masked + return false, nil + } else { + return false, err + } + } + if target != "/dev/null" { + // The symlink doesn't point to /dev/null, hence the unit isn't masked + return false, nil + } + } return true, nil } -func (ut Util) EnableUnit(enabledUnit string) error { - return ut.appendLineToPreset(fmt.Sprintf("enable %s", enabledUnit)) +func (ut Util) EnableUnit(enabledUnit string, scope UnitScope) error { + return ut.appendLineToPreset(fmt.Sprintf("enable %s", enabledUnit), ut.SystemdPresetPath(scope)) } -func (ut Util) DisableUnit(disabledUnit string) error { - return ut.appendLineToPreset(fmt.Sprintf("disable %s", disabledUnit)) +func (ut Util) DisableUnit(disabledUnit string, scope UnitScope) error { + return ut.appendLineToPreset(fmt.Sprintf("disable %s", disabledUnit), ut.SystemdPresetPath(scope)) } -func (ut Util) appendLineToPreset(data string) error { - path, err := ut.JoinPath(PresetPath) +func (ut Util) appendLineToPreset(data string, presetpath string) error { + path, err := ut.JoinPath(presetpath) if err != nil { return err } diff --git a/tests/positive/systemd/create_unit.go b/tests/positive/systemd/create_unit.go index 1964a60278..2be8c56203 100644 --- a/tests/positive/systemd/create_unit.go +++ b/tests/positive/systemd/create_unit.go @@ -21,6 +21,7 @@ import ( func init() { register.Register(register.PositiveTest, CreateSystemdService()) + register.Register(register.PositiveTest, CreateSystemdUserService()) } func CreateSystemdService() types.Test { @@ -63,3 +64,92 @@ func CreateSystemdService() types.Test { ConfigMinVersion: configMinVersion, } } + +func CreateSystemdUserService() types.Test { + name := "systemd.unit.userunit.create" + in := types.GetBaseDisk() + out := types.GetBaseDisk() + config := `{ + "ignition": { "version": "$version" }, + "systemd": { + "units": [{ + "name": "example.service", + "enabled": true, + "scope": "user", + "users": ["tester1", "tester2"], + "contents": "[Service]\nType=oneshot\nExecStart=/usr/bin/echo Hello World\n\n[Install]\nWantedBy=multi-user.target" + }, + { + "contents": "[Service]\nType=oneshot\nExecStart=/usr/bin/echo Hello World\n\n[Install]\nWantedBy=multi-user.target", + "enabled": true, + "name": "example.service", + "scope": "global" + }] + } + }` + configMinVersion := "3.4.0-experimental" + + in[0].Partitions.AddFiles("ROOT", []types.File{ + { + Node: types.Node{ + Name: "passwd", + Directory: "etc", + }, + Contents: "tester1:x:44:4242::/home/tester1:/bin/false\ntester2:x:45:4242::/home/tester2:/bin/false", + }, + { + Node: types.Node{ + Name: "nsswitch.conf", + Directory: "etc", + }, + Contents: "passwd: files\ngroup: files\nshadow: files\ngshadow: files\n", + }, + }) + + out[0].Partitions.AddFiles("ROOT", []types.File{ + { + Node: types.Node{ + Name: "example.service", + Directory: "home/tester1/.config/systemd/user", + }, + Contents: "[Service]\nType=oneshot\nExecStart=/usr/bin/echo Hello World\n\n[Install]\nWantedBy=multi-user.target", + }, + { + Node: types.Node{ + Name: "example.service", + Directory: "home/tester2/.config/systemd/user", + }, + Contents: "[Service]\nType=oneshot\nExecStart=/usr/bin/echo Hello World\n\n[Install]\nWantedBy=multi-user.target", + }, + { + Node: types.Node{ + Name: "21-ignition.preset", + Directory: "etc/systemd/user-preset", + }, + Contents: "enable example.service\n", + }, + + { + Node: types.Node{ + Name: "example.service", + Directory: "etc/systemd/user", + }, + Contents: "[Service]\nType=oneshot\nExecStart=/usr/bin/echo Hello World\n\n[Install]\nWantedBy=multi-user.target", + }, + { + Node: types.Node{ + Name: "20-ignition.preset", + Directory: "etc/systemd/user-preset", + }, + Contents: "enable example.service\n", + }, + }) + + return types.Test{ + Name: name, + In: in, + Out: out, + Config: config, + ConfigMinVersion: configMinVersion, + } +}