Skip to content

Commit 375c50d

Browse files
committed
Include server environment file in the backup
Even though this file can be empty, it must exists, so use the original in the backup.
1 parent 18a527a commit 375c50d

3 files changed

Lines changed: 290 additions & 4 deletions

File tree

mgradm/cmd/backup/create/systemd_utils.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2025 SUSE LLC
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

@@ -91,9 +91,23 @@ func gatherSystemdItems() []string {
9191
if err != nil {
9292
log.Debug().Err(err).Msgf("failed to get the path to the %s service configuration files", serviceName)
9393
} else {
94-
dropIns := strings.Split(dropIns, " ")
95-
result = append(result, filepath.Dir(dropIns[0]))
96-
result = append(result, dropIns[:]...)
94+
quoted := false
95+
dropIns := strings.FieldsFunc(dropIns, func(r rune) bool {
96+
if r == '"' {
97+
quoted = !quoted
98+
}
99+
return !quoted && r == ' '
100+
})
101+
if len(dropIns) > 0 {
102+
result = append(result, filepath.Dir(dropIns[0]))
103+
result = append(result, dropIns[:]...)
104+
}
105+
}
106+
107+
// Get the environment file
108+
envFile := path.Join(servicePath+".d", podman.ServerEnvironmentFile)
109+
if _, err := os.Stat(envFile); err == nil {
110+
result = append(result, envFile)
97111
}
98112
}
99113
return result
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package create
6+
7+
import (
8+
"archive/tar"
9+
"io"
10+
"os"
11+
"path/filepath"
12+
"testing"
13+
14+
"github.com/uyuni-project/uyuni-tools/shared/podman"
15+
"github.com/uyuni-project/uyuni-tools/shared/testutils"
16+
"github.com/uyuni-project/uyuni-tools/shared/types"
17+
"github.com/uyuni-project/uyuni-tools/shared/utils"
18+
)
19+
20+
func TestFindService(t *testing.T) {
21+
oldSystemd := systemd
22+
defer func() { systemd = oldSystemd }()
23+
24+
tests := []struct {
25+
name string
26+
serviceName string
27+
hasServiceMock func(string) bool
28+
expectedService string
29+
expectedSkip bool
30+
}{
31+
{
32+
name: "Service directly exists",
33+
serviceName: "uyuni-server",
34+
hasServiceMock: func(name string) bool {
35+
return name == "uyuni-server"
36+
},
37+
expectedService: "uyuni-server",
38+
expectedSkip: false,
39+
},
40+
{
41+
name: "Service exists as template service",
42+
serviceName: "uyuni-server",
43+
hasServiceMock: func(name string) bool {
44+
return name == "uyuni-server@"
45+
},
46+
expectedService: "uyuni-server@",
47+
expectedSkip: false,
48+
},
49+
{
50+
name: "Service does not exist",
51+
serviceName: "uyuni-server",
52+
hasServiceMock: func(_ string) bool {
53+
return false
54+
},
55+
expectedService: "uyuni-server@",
56+
expectedSkip: true,
57+
},
58+
}
59+
60+
for _, tt := range tests {
61+
t.Run(tt.name, func(t *testing.T) {
62+
installedServices := []string{}
63+
if tt.hasServiceMock(tt.serviceName) {
64+
installedServices = append(installedServices, tt.serviceName)
65+
}
66+
if tt.hasServiceMock(tt.serviceName + "@") {
67+
installedServices = append(installedServices, tt.serviceName+"@")
68+
}
69+
70+
systemd = podman.NewSystemdWithDriver(&testutils.FakeSystemdDriver{Installed: installedServices})
71+
service, skip := findService(tt.serviceName)
72+
testutils.AssertEquals(t, "service name mismatch", tt.expectedService, service)
73+
testutils.AssertEquals(t, "skip mismatch", tt.expectedSkip, skip)
74+
})
75+
}
76+
}
77+
78+
func TestGatherSystemdItems(t *testing.T) {
79+
oldSystemd := systemd
80+
oldUyuniServices := utils.UyuniServices
81+
defer func() {
82+
systemd = oldSystemd
83+
utils.UyuniServices = oldUyuniServices
84+
}()
85+
86+
// We'll use a single custom service for testing
87+
utils.UyuniServices = []types.UyuniService{
88+
{Name: "test-service"},
89+
}
90+
91+
tempDir := t.TempDir()
92+
93+
// Service is skipped (does not exist)
94+
systemd = podman.NewSystemdWithDriver(&testutils.FakeSystemdDriver{})
95+
items := gatherSystemdItems()
96+
testutils.AssertEquals(t, "items length should be 0", 0, len(items))
97+
98+
// Service exists, but FragmentPath fails
99+
systemd = podman.NewSystemdWithDriver(&testutils.FakeSystemdDriver{
100+
Installed: []string{"test-service"},
101+
})
102+
items = gatherSystemdItems()
103+
testutils.AssertEquals(t, "items length should be 0 on FragmentPath error", 0, len(items))
104+
105+
// Service exists, FragmentPath succeeds, but DropInPaths fails, and no env file
106+
servicePath := filepath.Join(tempDir, "test-service.service")
107+
systemd = podman.NewSystemdWithDriver(&testutils.FakeSystemdDriver{
108+
Installed: []string{"test-service"},
109+
ServiceProperties: map[string]map[string]string{
110+
"test-service": {
111+
podman.FragmentPath: servicePath,
112+
},
113+
},
114+
})
115+
items = gatherSystemdItems()
116+
expectedItems := []string{servicePath}
117+
testutils.AssertEquals(t, "items should only contain service path", expectedItems, items)
118+
119+
// Service exists, FragmentPath succeeds, DropInPaths is present but empty, and no env file
120+
systemd = podman.NewSystemdWithDriver(&testutils.FakeSystemdDriver{
121+
Installed: []string{"test-service"},
122+
ServiceProperties: map[string]map[string]string{
123+
"test-service": {
124+
podman.FragmentPath: servicePath,
125+
podman.DropInPaths: "",
126+
},
127+
},
128+
})
129+
items = gatherSystemdItems()
130+
expectedItems = []string{servicePath}
131+
testutils.AssertEquals(t, "items should only contain service path when DropInPaths is empty", expectedItems, items)
132+
133+
// Service exists, FragmentPath succeeds, DropInPaths succeeds, and env file exists
134+
serviceDir := servicePath + ".d"
135+
err := os.MkdirAll(serviceDir, 0755)
136+
if err != nil {
137+
t.Fatal(err)
138+
}
139+
140+
envFile := filepath.Join(serviceDir, podman.ServerEnvironmentFile)
141+
err = os.WriteFile(envFile, []byte("some env"), 0644)
142+
if err != nil {
143+
t.Fatal(err)
144+
}
145+
146+
dropIn1 := filepath.Join(serviceDir, "10-test.conf")
147+
dropIn2 := filepath.Join(serviceDir, "20-test.conf")
148+
// Test with a drop-in file that has spaces in its name. Systemd adds quotes around such paths
149+
dropIn3 := filepath.Join(serviceDir, "\"30 with space test.conf\"")
150+
dropInPaths := dropIn1 + " " + dropIn2 + " " + dropIn3
151+
152+
systemd = podman.NewSystemdWithDriver(&testutils.FakeSystemdDriver{
153+
Installed: []string{"test-service"},
154+
ServiceProperties: map[string]map[string]string{
155+
"test-service": {
156+
podman.FragmentPath: servicePath,
157+
podman.DropInPaths: dropInPaths,
158+
},
159+
},
160+
})
161+
162+
items = gatherSystemdItems()
163+
expectedItems = []string{
164+
servicePath,
165+
serviceDir,
166+
dropIn1,
167+
dropIn2,
168+
dropIn3,
169+
envFile,
170+
}
171+
testutils.AssertEquals(t, "items mismatch", expectedItems, items)
172+
}
173+
174+
func TestExportSystemdConfiguration(t *testing.T) {
175+
oldSystemd := systemd
176+
oldUyuniServices := utils.UyuniServices
177+
defer func() {
178+
systemd = oldSystemd
179+
utils.UyuniServices = oldUyuniServices
180+
}()
181+
182+
tempDir := t.TempDir()
183+
184+
serviceFile := filepath.Join(tempDir, "fake-service.service")
185+
serviceContent := "[Service]\nExecStart=/usr/bin/fake"
186+
err := os.WriteFile(serviceFile, []byte(serviceContent), 0644)
187+
if err != nil {
188+
t.Fatal(err)
189+
}
190+
191+
serviceDir := serviceFile + ".d"
192+
err = os.MkdirAll(serviceDir, 0755)
193+
if err != nil {
194+
t.Fatal(err)
195+
}
196+
197+
dropInFile := filepath.Join(serviceDir, "custom.conf")
198+
dropInContent := "[Service]\nEnvironment=FOO=bar"
199+
err = os.WriteFile(dropInFile, []byte(dropInContent), 0644)
200+
if err != nil {
201+
t.Fatal(err)
202+
}
203+
204+
utils.UyuniServices = []types.UyuniService{
205+
{Name: "fake-service"},
206+
}
207+
208+
systemd = podman.NewSystemdWithDriver(&testutils.FakeSystemdDriver{
209+
Installed: []string{"fake-service"},
210+
ServiceProperties: map[string]map[string]string{
211+
"fake-service": {
212+
podman.FragmentPath: serviceFile,
213+
podman.DropInPaths: dropInFile,
214+
},
215+
},
216+
})
217+
218+
outputDir := t.TempDir()
219+
220+
// dryRun = true
221+
err = exportSystemdConfiguration(outputDir, true)
222+
testutils.AssertNoError(t, "exportSystemdConfiguration with dryRun=true failed", err)
223+
224+
backupFilePath := filepath.Join(outputDir, "systemdBackup.tar")
225+
if _, err := os.Stat(backupFilePath); err == nil {
226+
t.Error("backup file should not have been created on dryRun=true")
227+
}
228+
229+
// dryRun = false
230+
err = exportSystemdConfiguration(outputDir, false)
231+
testutils.AssertNoError(t, "exportSystemdConfiguration with dryRun=false failed", err)
232+
233+
if _, err := os.Stat(backupFilePath); os.IsNotExist(err) {
234+
t.Fatal("backup file was not created on dryRun=false")
235+
}
236+
237+
tarFile, err := os.Open(backupFilePath)
238+
if err != nil {
239+
t.Fatal(err)
240+
}
241+
defer tarFile.Close()
242+
243+
tr := tar.NewReader(tarFile)
244+
foundFiles := make(map[string]string)
245+
246+
for {
247+
header, err := tr.Next()
248+
if err == io.EOF {
249+
break
250+
}
251+
if err != nil {
252+
t.Fatal(err)
253+
}
254+
255+
if header.FileInfo().IsDir() {
256+
foundFiles[header.Name] = "dir"
257+
} else {
258+
content, err := io.ReadAll(tr)
259+
if err != nil {
260+
t.Fatal(err)
261+
}
262+
foundFiles[header.Name] = string(content)
263+
}
264+
}
265+
266+
expectedServiceDirName := serviceDir + "/"
267+
testutils.AssertEquals(t, "service file content", serviceContent, foundFiles[serviceFile])
268+
testutils.AssertEquals(t, "service dir entry type", "dir", foundFiles[expectedServiceDirName])
269+
testutils.AssertEquals(t, "drop-in file content", dropInContent, foundFiles[dropInFile])
270+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Include server environment file in the backup
2+
(bsc#1268649)

0 commit comments

Comments
 (0)