Skip to content

Commit b4ea81d

Browse files
Merge pull request #5086 from cheesesashimi/zzlotnik/OCPBUGS-56648
OCPBUGS-56648: fixes systemd unit creation for empty units
2 parents d151372 + b14bd5c commit b4ea81d

File tree

4 files changed

+251
-7
lines changed

4 files changed

+251
-7
lines changed

pkg/daemon/file_writers.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,8 @@ func writeUnit(u ign3types.Unit, systemdRoot string, isCoreOSVariant bool) error
290290
return nil
291291
}
292292

293-
if u.Contents != nil && *u.Contents != "" {
293+
switch {
294+
case unitHasContent(u):
294295
klog.Infof("Writing systemd unit %q", u.Name)
295296
if _, err := os.Stat(withUsrPath(fpath)); err == nil &&
296297
isCoreOSVariant {
@@ -315,7 +316,7 @@ func writeUnit(u ign3types.Unit, systemdRoot string, isCoreOSVariant bool) error
315316
}
316317

317318
klog.V(2).Infof("Successfully wrote systemd unit %q: ", u.Name)
318-
} else if u.Mask != nil && !*u.Mask {
319+
case u.Mask != nil && !*u.Mask:
319320
// if mask is explicitly set to false, make sure to remove a previous mask
320321
// see https://bugzilla.redhat.com/show_bug.cgi?id=1966445
321322
// Note that this does not catch all cleanup cases; for example, if the previous machine config specified
@@ -325,11 +326,20 @@ func writeUnit(u ign3types.Unit, systemdRoot string, isCoreOSVariant bool) error
325326
if err := os.RemoveAll(fpath); err != nil {
326327
return fmt.Errorf("failed to cleanup %s: %w", fpath, err)
327328
}
329+
default:
330+
klog.Infof("Unit %q has no content, skipping write", u.Name)
328331
}
329332

330333
return nil
331334
}
332335

336+
// Determines if a systemd unit has contents by first checking whether the
337+
// contents field is nil and then checking if the contents field is an empty
338+
// string.
339+
func unitHasContent(u ign3types.Unit) bool {
340+
return u.Contents != nil && *u.Contents != ""
341+
}
342+
333343
// writeUnits writes systemd units and their dropins to disk
334344
func writeUnits(units []ign3types.Unit, systemdRoot string, isCoreOSVariant bool) error {
335345
for _, u := range units {

pkg/daemon/update.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,10 +2124,20 @@ func (dn *Daemon) writeUnits(units []ign3types.Unit) error {
21242124
// to not go through.
21252125

21262126
if u.Enabled != nil {
2127-
if *u.Enabled {
2128-
enabledUnits = append(enabledUnits, u.Name)
2127+
// Only when a unit has contents should we attempt to enable or disable it.
2128+
// See: https://issues.redhat.com/browse/OCPBUGS-56648
2129+
if unitHasContent(u) {
2130+
if *u.Enabled {
2131+
enabledUnits = append(enabledUnits, u.Name)
2132+
} else {
2133+
disabledUnits = append(disabledUnits, u.Name)
2134+
}
21292135
} else {
2130-
disabledUnits = append(disabledUnits, u.Name)
2136+
action := "disable"
2137+
if *u.Enabled {
2138+
action = "enable"
2139+
}
2140+
klog.Infof("Could not %s unit %q, because it has no contents, skipping", action, u.Name)
21312141
}
21322142
} else {
21332143
if err := dn.presetUnit(u); err != nil {

test/e2e-1of2/mcd_test.go

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ func TestDontDeleteRPMFiles(t *testing.T) {
620620
func TestIgn3Cfg(t *testing.T) {
621621
cs := framework.NewClientSet("")
622622

623-
delete := helpers.CreateMCP(t, cs, "infra")
623+
deleteFunc := helpers.CreateMCP(t, cs, "infra")
624624
workerOldMc := helpers.GetMcName(t, cs, "worker")
625625

626626
unlabelFunc := helpers.LabelRandomNodeFromPool(t, cs, "worker", "node-role.kubernetes.io/infra")
@@ -631,7 +631,7 @@ func TestIgn3Cfg(t *testing.T) {
631631
if err := helpers.WaitForPoolComplete(t, cs, "worker", workerOldMc); err != nil {
632632
t.Fatal(err)
633633
}
634-
delete()
634+
deleteFunc()
635635
})
636636
// create a dummy MC with an sshKey for user Core
637637
mcName := fmt.Sprintf("99-ign3cfg-infra-%s", uuid.NewUUID())
@@ -650,6 +650,59 @@ func TestIgn3Cfg(t *testing.T) {
650650
tempFile := ign3types.File{Node: ign3types.Node{Path: "/etc/testfileconfig"},
651651
FileEmbedded1: ign3types.FileEmbedded1{Contents: ign3types.Resource{Source: &testfiledata}, Mode: &mode}}
652652
testIgn3Config.Storage.Files = append(testIgn3Config.Storage.Files, tempFile)
653+
654+
overrideName := "override.conf"
655+
testIgn3Config.Systemd.Units = []ign3types.Unit{
656+
{
657+
Name: "new-svc.service",
658+
Enabled: helpers.BoolToPtr(true),
659+
Contents: getSystemdUnitContents("New Service"),
660+
},
661+
{
662+
Name: "new-svc-with-dropin.service",
663+
Enabled: helpers.BoolToPtr(true),
664+
Contents: getSystemdUnitContents("New Service With Dropin"),
665+
Dropins: []ign3types.Dropin{
666+
{
667+
Name: overrideName,
668+
Contents: helpers.StrToPtr("[Unit]\nDescription=Dropin Override"),
669+
},
670+
},
671+
},
672+
{
673+
Name: "new-empty-svc.service",
674+
Enabled: helpers.BoolToPtr(true),
675+
},
676+
{
677+
Name: "new-empty-svc-with-empty-dropin.service",
678+
Enabled: helpers.BoolToPtr(true),
679+
Dropins: []ign3types.Dropin{
680+
{
681+
Name: overrideName,
682+
},
683+
},
684+
},
685+
{
686+
Name: "new-empty-svc-with-dropin.service",
687+
Enabled: helpers.BoolToPtr(true),
688+
Dropins: []ign3types.Dropin{
689+
{
690+
Name: overrideName,
691+
Contents: getSystemdUnitContents("Empty Service With Dropin Override"),
692+
},
693+
},
694+
},
695+
{
696+
Name: "new-empty-disabled-svc.service",
697+
Enabled: helpers.BoolToPtr(false),
698+
},
699+
{
700+
Name: "new-disabled-svc.service",
701+
Enabled: helpers.BoolToPtr(false),
702+
Contents: getSystemdUnitContents("New Disabled Service"),
703+
},
704+
}
705+
653706
rawIgnConfig := helpers.MarshalOrDie(testIgn3Config)
654707
mcadd.Spec.Config.Raw = rawIgnConfig
655708

@@ -682,6 +735,46 @@ func TestIgn3Cfg(t *testing.T) {
682735
}
683736
t.Logf("Node %s has file", infraNode.Name)
684737

738+
systemdUnitAssertions := map[string]func(*testing.T, string){
739+
"new-svc.service": func(t *testing.T, name string) {
740+
assertSystemdUnitFileExists(t, cs, infraNode, name)
741+
assertSystemdUnitIsEnabled(t, cs, infraNode, name)
742+
},
743+
"new-svc-with-dropin.service": func(t *testing.T, name string) {
744+
assertSystemdUnitFileExists(t, cs, infraNode, name)
745+
assertSystemdUnitIsEnabled(t, cs, infraNode, name)
746+
assertSystemdUnitDropinFileExists(t, cs, infraNode, name, overrideName)
747+
assertSystemdUnitHasDropins(t, cs, infraNode, name, []string{overrideName})
748+
},
749+
"new-empty-svc.service": func(t *testing.T, name string) {
750+
assertSystemdUnitFileDoesNotExist(t, cs, infraNode, name)
751+
assertSystemdUnitDoesNotExist(t, cs, infraNode, name)
752+
},
753+
"new-empty-svc-with-empty-dropin.service": func(t *testing.T, name string) {
754+
assertSystemdUnitDoesNotExist(t, cs, infraNode, name)
755+
assertSystemdUnitFileDoesNotExist(t, cs, infraNode, name)
756+
assertSystemdUnitDropinFileDoesNotExist(t, cs, infraNode, name, overrideName)
757+
},
758+
"new-empty-svc-with-dropin.service": func(t *testing.T, name string) {
759+
assertSystemdUnitFileDoesNotExist(t, cs, infraNode, name)
760+
assertSystemdUnitDropinFileExists(t, cs, infraNode, name, overrideName)
761+
},
762+
"new-empty-disabled-svc.service": func(t *testing.T, name string) {
763+
assertSystemdUnitDoesNotExist(t, cs, infraNode, name)
764+
assertSystemdUnitFileDoesNotExist(t, cs, infraNode, name)
765+
},
766+
"new-disabled-svc.service": func(t *testing.T, name string) {
767+
assertSystemdUnitFileExists(t, cs, infraNode, name)
768+
assertSystemdUnitExists(t, cs, infraNode, name)
769+
assertSystemdUnitIsDisabled(t, cs, infraNode, name)
770+
},
771+
}
772+
773+
for name, assertionFunc := range systemdUnitAssertions {
774+
t.Logf("Running assertion(s) for %s", name)
775+
assertionFunc(t, name)
776+
}
777+
685778
unlabelFunc()
686779

687780
workerMCP, err := cs.MachineConfigPools().Get(context.TODO(), "worker", metav1.GetOptions{})
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package e2e_1of2_test
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
corev1 "k8s.io/api/core/v1"
11+
12+
"github.com/openshift/machine-config-operator/test/framework"
13+
"github.com/openshift/machine-config-operator/test/helpers"
14+
)
15+
16+
// Creates a dummy systemd unit file with a customizable description.
17+
func getSystemdUnitContents(desc string) *string {
18+
contents := []string{
19+
"[Unit]",
20+
fmt.Sprintf("Description=%s", desc),
21+
"",
22+
"[Service]",
23+
`ExecStart=/bin/bash -c "sleep infinity"`,
24+
"Restart=on-failure",
25+
"",
26+
"[Install]",
27+
"WantedBy=multi-user.target",
28+
}
29+
30+
return helpers.StrToPtr(strings.Join(contents, "\n"))
31+
}
32+
33+
// Asserts that a given systemd unit is enabled.
34+
func assertSystemdUnitIsEnabled(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
35+
t.Helper()
36+
37+
output := showSystemdUnit(t, cs, node, unitName)
38+
assert.Contains(t, output, "UnitFileState=enabled")
39+
assert.Contains(t, output, "ActiveState=active")
40+
assert.Contains(t, output, "LoadState=loaded")
41+
}
42+
43+
// Asserts that a given systemd unit is disabled.
44+
func assertSystemdUnitIsDisabled(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
45+
t.Helper()
46+
47+
output := showSystemdUnit(t, cs, node, unitName)
48+
assert.Contains(t, output, "UnitFileState=disabled")
49+
assert.Contains(t, output, "ActiveState=inactive")
50+
assert.Contains(t, output, "LoadState=loaded")
51+
}
52+
53+
// Asserts that a given systemd unit has the specified dropins.
54+
func assertSystemdUnitHasDropins(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string, dropins []string) {
55+
t.Helper()
56+
57+
output := showSystemdUnit(t, cs, node, unitName)
58+
dropinPaths := getSystemdLineFromOutput(output, "DropInPaths")
59+
for _, dropin := range dropins {
60+
assert.Contains(t, dropinPaths, dropin)
61+
}
62+
}
63+
64+
const (
65+
// The root path for where systemd stores its units, dropins, etc.
66+
systemdRootPath string = "/etc/systemd/system"
67+
)
68+
69+
// Asserts that a dropin file exists for a given systemd unit.
70+
func assertSystemdUnitDropinFileExists(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName, dropinName string) {
71+
t.Helper()
72+
helpers.AssertFileOnNode(t, cs, node, filepath.Join(systemdRootPath, unitName+".d", dropinName))
73+
}
74+
75+
// Asserts tha a dropin file does not exist for a given systemd unit.
76+
func assertSystemdUnitDropinFileDoesNotExist(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName, dropinName string) {
77+
t.Helper()
78+
helpers.AssertFileNotOnNode(t, cs, node, filepath.Join(systemdRootPath, unitName+".d", dropinName))
79+
}
80+
81+
// Asserts that the unit file for a given systemd unit exists.
82+
func assertSystemdUnitFileExists(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
83+
t.Helper()
84+
helpers.AssertFileOnNode(t, cs, node, filepath.Join(systemdRootPath, unitName))
85+
}
86+
87+
// Asserts that the given systemd unit exists by querying systemd.
88+
func assertSystemdUnitExists(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
89+
t.Helper()
90+
output := showSystemdUnit(t, cs, node, unitName)
91+
loadState := getSystemdLineFromOutput(output, "LoadState")
92+
assert.Equal(t, loadState, "LoadState=loaded")
93+
}
94+
95+
// Asserts that the given systemd unit does not exist by querying systemd.
96+
func assertSystemdUnitDoesNotExist(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
97+
t.Helper()
98+
99+
output := showSystemdUnit(t, cs, node, unitName)
100+
101+
loadState := getSystemdLineFromOutput(output, "LoadState")
102+
assert.Equal(t, "LoadState=not-found", loadState)
103+
104+
loadError := getSystemdLineFromOutput(output, "LoadError")
105+
assert.Contains(t, loadError, "NoSuchUnit")
106+
}
107+
108+
// Asserts that the given systemd unit file does not exist.
109+
func assertSystemdUnitFileDoesNotExist(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) {
110+
t.Helper()
111+
helpers.AssertFileNotOnNode(t, cs, node, filepath.Join("/rootfs", systemdRootPath, unitName))
112+
}
113+
114+
// Runs "systemctl show <unit>" on a given node. Note: This command should
115+
// always returns zero even when the unit does not exist. Assertions are done
116+
// by interrogating its output.
117+
func showSystemdUnit(t *testing.T, cs *framework.ClientSet, node corev1.Node, unitName string) string {
118+
return helpers.ExecCmdOnNode(t, cs, node, "chroot", "/rootfs", "systemctl", "show", unitName)
119+
}
120+
121+
// TODO: This does not handle the case where a key may appear multiple times;
122+
// e.g., multiple ExecStart lines.
123+
func getSystemdLineFromOutput(output, prefix string) string {
124+
for _, line := range strings.Split(output, "\n") {
125+
if strings.HasPrefix(line, prefix) {
126+
return line
127+
}
128+
}
129+
130+
return ""
131+
}

0 commit comments

Comments
 (0)