Skip to content

Commit

Permalink
Add support for user units
Browse files Browse the repository at this point in the history
Allow users to create and enable user-level systemd services in ignition

Fixes #1296
  • Loading branch information
Nemric authored and bgilbert committed Jan 13, 2022
1 parent 191dc87 commit 01a8037
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 34 deletions.
1 change: 1 addition & 0 deletions config/shared/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
41 changes: 40 additions & 1 deletion config/v3_4_experimental/types/unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package types
import (
"fmt"
"path"
"path/filepath"
"strings"

"github.com/coreos/ignition/v2/config/shared/errors"
Expand All @@ -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
}
Expand All @@ -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":
Expand Down Expand Up @@ -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")
}
}
14 changes: 13 additions & 1 deletion internal/exec/stages/files/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
}
Expand All @@ -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
}
52 changes: 37 additions & 15 deletions internal/exec/stages/files/units.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package files
import (
"fmt"
"path/filepath"
"sort"
"strings"

"github.com/coreos/ignition/v2/config/shared/errors"
Expand All @@ -33,6 +34,7 @@ type Preset struct {
enabled bool
instantiatable bool
instances []string
path string
}

// warnOnOldSystemdVersion checks the version of Systemd
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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
Expand All @@ -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
}

Expand Down
120 changes: 120 additions & 0 deletions internal/exec/stages/files/units_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
}
}
25 changes: 21 additions & 4 deletions internal/exec/util/path.go
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Loading

0 comments on commit 01a8037

Please sign in to comment.