From 01a8037738ed0e8c8e59c234f766e1ea8748f34c Mon Sep 17 00:00:00 2001 From: Nemric Date: Wed, 12 Jan 2022 15:09:12 +0000 Subject: [PATCH] Add support for user units Allow users to create and enable user-level systemd services in ignition Fixes #1296 --- config/shared/errors/errors.go | 1 + config/v3_4_experimental/types/unit.go | 41 +++++++- internal/exec/stages/files/files.go | 14 ++- internal/exec/stages/files/units.go | 52 +++++++--- internal/exec/stages/files/units_test.go | 120 +++++++++++++++++++++++ internal/exec/util/path.go | 25 ++++- internal/exec/util/unit.go | 26 ++--- 7 files changed, 245 insertions(+), 34 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 7761280d0..52f7759f8 100644 --- a/config/shared/errors/errors.go +++ b/config/shared/errors/errors.go @@ -85,6 +85,7 @@ 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("invalid unit scope (system, user)") // Misc errors ErrSourceRequired = errors.New("source is required") diff --git a/config/v3_4_experimental/types/unit.go b/config/v3_4_experimental/types/unit.go index bc2d3299c..116320a64 100644 --- a/config/v3_4_experimental/types/unit.go +++ b/config/v3_4_experimental/types/unit.go @@ -17,6 +17,7 @@ package types import ( "fmt" "path" + "path/filepath" "strings" "github.com/coreos/ignition/v2/config/shared/errors" @@ -28,6 +29,13 @@ import ( "github.com/coreos/vcontext/report" ) +type UnitScope string + +const ( + SystemUnit UnitScope = "system" + UserUnit UnitScope = "user" +) + func (u Unit) Key() string { return u.Name } @@ -43,10 +51,22 @@ 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)) return } +func validateScope(scope *string) error { + if scope == nil { + return nil + } + switch *scope { + case "system", "user": + return nil + default: + return errors.ErrInvalidUnitScope + } +} + func validateName(name string) error { switch path.Ext(name) { case ".service", ".socket", ".device", ".mount", ".automount", ".swap", ".target", ".path", ".timer", ".snapshot", ".slice", ".scope": @@ -80,3 +100,22 @@ func validateUnitContent(content *string) ([]*unit.UnitOption, error) { } return opts, nil } + +func (u Unit) GetScope() UnitScope { + if u.Scope == nil { + return UnitScope(*util.StrToPtr("system")) + } else { + return UnitScope(*u.Scope) + } +} + +func (u Unit) GetBasePath() string { + switch u.GetScope() { + case UserUnit: + return filepath.Join("etc", "systemd", "user") + case SystemUnit: + return filepath.Join("etc", "systemd", "system") + default: + return filepath.Join("etc", "systemd", "system") + } +} diff --git a/internal/exec/stages/files/files.go b/internal/exec/stages/files/files.go index e08514403..1a9c21da2 100644 --- a/internal/exec/stages/files/files.go +++ b/internal/exec/stages/files/files.go @@ -121,7 +121,9 @@ func (s *stage) relabeling() bool { func (s *stage) relabel(paths ...string) { if s.toRelabel != nil { for _, path := range paths { - s.toRelabel = append(s.toRelabel, filepath.Join(s.DestDir, path)) + if !s.ToRelabelContains(path) { + s.toRelabel = append(s.toRelabel, filepath.Join(s.DestDir, path)) + } } } } @@ -141,3 +143,13 @@ func (s *stage) relabelFiles() error { return s.RelabelFiles(s.toRelabel) } + +//add a check for not relabelling path more than once +func (s *stage) ToRelabelContains(value string) bool { + for _, val := range s.toRelabel { + if val == value { + return true + } + } + return false +} diff --git a/internal/exec/stages/files/units.go b/internal/exec/stages/files/units.go index f6f720ca4..3922d6582 100644 --- a/internal/exec/stages/files/units.go +++ b/internal/exec/stages/files/units.go @@ -17,6 +17,7 @@ package files import ( "fmt" "path/filepath" + "sort" "strings" "github.com/coreos/ignition/v2/config/shared/errors" @@ -33,6 +34,7 @@ type Preset struct { enabled bool instantiatable bool instances []string + path string } // warnOnOldSystemdVersion checks the version of Systemd @@ -73,12 +75,12 @@ func (s *stage) createUnits(config types.Config) error { 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.SystemdPresetPath(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{}} + presets[key] = &Preset{unit.Name, *unit.Enabled, false, []string{}, util.SystemdPresetPath(unit)} } else { return fmt.Errorf("%q key is already present in the presets map", key) } @@ -118,10 +120,11 @@ 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 { + // useless as it will be done later in createSystemdPresetFiles + // if err := s.relabelPath(filepath.Join(s.DestDir, util.PresetPath)); err != nil { + // return err + // } + if err := s.createSystemdPresetFiles(presets); err != nil { return err } } @@ -148,27 +151,27 @@ 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 { +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 { + 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.path) }, "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.path) }, "setting preset to disabled for %q", unitString, ); err != nil { return err @@ -183,7 +186,26 @@ func (s *stage) createSystemdPresetFile(presets map[string]*Preset) error { return err } } - s.relabel(util.PresetPath) + + //getting all paths from presets + var paths []string + for _, preset := range presets { + paths = append(paths, preset.path) + } + sort.Slice(paths, func(i, j int) bool { + return paths[i] < paths[j] + }) + //running accros differents paths not to apply them s.relabalpath more than once + var tmppath string = "" + for _, path := range paths { + if path != tmppath { + if err := s.relabelPath(filepath.Join(s.DestDir, path)); err != nil { + return err + } + } + tmppath = path + } + return nil } diff --git a/internal/exec/stages/files/units_test.go b/internal/exec/stages/files/units_test.go index b7020f7ef..668de08ef 100644 --- a/internal/exec/stages/files/units_test.go +++ b/internal/exec/stages/files/units_test.go @@ -15,11 +15,16 @@ package files import ( + "fmt" "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 +79,118 @@ func TestParseInstanceUnit(t *testing.T) { } } } + +func TestSystemdUnitPath(t *testing.T) { + + tests := []struct { + in types.Unit + out string + }{ + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("system")}, + "etc/systemd/system", + }, + { + types.Unit{Name: "test.service"}, + "etc/systemd/system", + }, + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("user")}, + "etc/systemd/user", + }, + } + + for i, test := range tests { + path := util.SystemdUnitsPath(test.in) + if path != test.out { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, path) + } + } +} + +func TestSystemdDropinsPath(t *testing.T) { + + tests := []struct { + in types.Unit + out string + }{ + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("system")}, + "etc/systemd/system/test.service.d", + }, + { + types.Unit{Name: "test.service"}, + "etc/systemd/system/test.service.d", + }, + { + types.Unit{Name: "test.service", Scope: cfgutil.StrToPtr("user")}, + "etc/systemd/user/test.service.d", + }, + } + + for i, test := range tests { + path := util.SystemdDropinsPath(test.in) + if path != test.out { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, path) + } + } +} + +func TestSystemdPresetPath(t *testing.T) { + + 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/20-ignition.preset", + }, + } + + for i, test := range tests { + path := util.SystemdPresetPath(test.in) + if path != test.out { + t.Errorf("#%d: bad error: want %v, got %v", i, test.out, path) + } + } +} + +func TestCreateUnits(t *testing.T) { + + config, report, err := v3_4_experimental.Parse([]byte(`{"ignition":{"version":"3.4.0-experimental"},"systemd":{"units":[{"contents":"[Unit]\nDescription=Prometheus node exporter\n[Install]\nWantedBy=multi-user.target\n","enabled":true,"name":"exporter.service"},{"contents":"[Unit]\nDescription=promtail.service\n[Install]\nWantedBy=multi-user.target default.target","enabled":true,"name":"promtail.service","scope":"user"},{"contents":"[Unit]\nDescription=promtail.service\n[Install]\nWantedBy=multi-user.target default.target","enabled":true,"name":"grafana.service","scope":"system"}]}}`)) + + if err != nil { + print(report.Entries, err.Error()) + } + + fmt.Printf("config: %v\n", config) + + tests := []struct { + in types.Config + out error + }{ + { + config, + nil, + }, + } + + for i, test := range tests { + var logg log.Logger = log.New(true) + var st stage + st.Logger = &logg + test.out = st.createUnits(test.in) + if test.out != nil { + t.Errorf("#%d: error occured: %v", i, test.out) + } + } +} diff --git a/internal/exec/util/path.go b/internal/exec/util/path.go old mode 100644 new mode 100755 index 73f96f84d..91ace6741 --- a/internal/exec/util/path.go +++ b/internal/exec/util/path.go @@ -16,12 +16,29 @@ 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 SystemdUnitsPath(unit types.Unit) string { + return unit.GetBasePath() +} + +func SystemdPresetPath(unit types.Unit) string { + switch unit.GetScope() { + case types.UserUnit: + return filepath.Join("etc", "systemd", "user-preset", "20-ignition.preset") + case types.SystemUnit: + return filepath.Join("etc", "systemd", "system-preset", "20-ignition.preset") + default: + return filepath.Join("etc", "systemd", "system-preset", "20-ignition.preset") + } +} + +func SystemdWantsPath(unit types.Unit) string { + return filepath.Join(SystemdUnitsPath(unit), unit.Name+".wants") } -func SystemdDropinsPath(unitName string) string { - return filepath.Join("etc", "systemd", "system", unitName+".d") +func SystemdDropinsPath(unit types.Unit) string { + return filepath.Join(SystemdUnitsPath(unit), unit.Name+".d") } diff --git a/internal/exec/util/unit.go b/internal/exec/util/unit.go old mode 100644 new mode 100755 index 01321cfa8..305c21be1 --- a/internal/exec/util/unit.go +++ b/internal/exec/util/unit.go @@ -27,7 +27,7 @@ import ( ) const ( - PresetPath string = "/etc/systemd/system-preset/20-ignition.preset" + // PresetPath string = "/etc/systemd/system-preset/20-ignition.preset" DefaultPresetPermissions os.FileMode = 0644 ) @@ -41,7 +41,7 @@ func (ut Util) FileFromSystemdUnit(unit types.Unit) (FetchOp, error) { return FetchOp{}, err } - path, err := ut.JoinPath(SystemdUnitsPath(), unit.Name) + path, err := ut.JoinPath(SystemdUnitsPath(unit), unit.Name) if err != nil { return FetchOp{}, err } @@ -64,7 +64,7 @@ func (ut Util) FileFromSystemdUnitDropin(unit types.Unit, dropin types.Dropin) ( return FetchOp{}, err } - path, err := ut.JoinPath(SystemdDropinsPath(string(unit.Name)), dropin.Name) + path, err := ut.JoinPath(SystemdDropinsPath(unit), dropin.Name) if err != nil { return FetchOp{}, err } @@ -80,7 +80,7 @@ func (ut Util) FileFromSystemdUnitDropin(unit types.Unit, dropin types.Dropin) ( // 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) + path, err := ut.JoinPath(SystemdUnitsPath(unit), unit.Name) if err != nil { return "", err } @@ -95,12 +95,12 @@ func (ut Util) MaskUnit(unit types.Unit) (string, error) { return "", err } // not the same as the path above, since this lacks the sysroot prefix - return filepath.Join("/", SystemdUnitsPath(), unit.Name), nil + return filepath.Join("/", SystemdUnitsPath(unit), unit.Name), 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) + path, err := ut.JoinPath(SystemdUnitsPath(unit), unit.Name) if err != nil { return err } @@ -120,7 +120,7 @@ func (ut Util) UnmaskUnit(unit types.Unit) error { // 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) + path, err := ut.JoinPath(SystemdUnitsPath(unit), unit.Name) if err != nil { return false, err } @@ -146,16 +146,16 @@ func (ut Util) IsUnitMasked(unit types.Unit) (bool, error) { return true, nil } -func (ut Util) EnableUnit(enabledUnit string) error { - return ut.appendLineToPreset(fmt.Sprintf("enable %s", enabledUnit)) +func (ut Util) EnableUnit(enabledUnit string, presetpath string) error { + return ut.appendLineToPreset(fmt.Sprintf("enable %s", enabledUnit), presetpath) } -func (ut Util) DisableUnit(disabledUnit string) error { - return ut.appendLineToPreset(fmt.Sprintf("disable %s", disabledUnit)) +func (ut Util) DisableUnit(disabledUnit string, presetpath string) error { + return ut.appendLineToPreset(fmt.Sprintf("disable %s", disabledUnit), presetpath) } -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 }