Skip to content

Commit d9c2041

Browse files
authored
Merge pull request #614 from cbosdo/backup-fix
Fix backup handling of images and systemd files.
2 parents 5a53a69 + 8f3870c commit d9c2041

12 files changed

Lines changed: 173 additions & 38 deletions

File tree

mgradm/cmd/backup/create/create.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,16 @@ func gatherContainerImagesToBackup(skipImages bool) []string {
161161

162162
if !skipImages {
163163
for _, service := range utils.UyuniServices {
164-
if present, err := podman.IsImagePresent(service.Image.Name); err == nil && len(present) > 0 {
165-
images = append(images, service.Image.Name)
164+
serviceName, skip := findService(service.Name)
165+
if skip {
166+
continue
167+
}
168+
image := podman.GetServiceImage(serviceName)
169+
if image != "" {
170+
present, err := podman.IsImagePresent(image)
171+
if err == nil && len(present) > 0 {
172+
images = append(images, image)
173+
}
166174
}
167175
}
168176
}

mgradm/cmd/backup/create/systemd_utils.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"io"
1111
"os"
1212
"path"
13+
"strings"
1314

1415
"github.com/rs/zerolog/log"
1516
backup "github.com/uyuni-project/uyuni-tools/mgradm/cmd/backup/shared"
@@ -76,20 +77,20 @@ func gatherSystemdItems() []string {
7677
continue
7778
}
7879

79-
result = append(result, podman.GetServicePath(serviceName))
80-
// For single mandatory replica following returns 0 so loop is skipped
81-
for i := 0; i < systemd.CurrentReplicaCount(service.Name); i++ {
82-
result = append(result, podman.GetServicePath(fmt.Sprintf("%s%d", serviceName, i)))
83-
}
84-
serviceConfDir := podman.GetServiceConfFolder(serviceName)
85-
serviceFiles, err := os.ReadDir(serviceConfDir)
80+
servicePath, err := systemd.GetServiceProperty(serviceName, podman.FragmentPath)
8681
if err != nil {
87-
log.Debug().Msgf("Service configuration directory %s not found, skipping", serviceConfDir)
82+
log.Debug().Err(err).Msgf("failed to get the path to the %s service file", serviceName)
83+
// Skipping the dropins since we would likely get a similar error.
8884
continue
8985
}
90-
result = append(result, serviceConfDir)
91-
for _, entry := range serviceFiles {
92-
result = append(result, path.Join(serviceConfDir, entry.Name()))
86+
result = append(result, servicePath)
87+
88+
// Get the drop in files
89+
dropIns, err := systemd.GetServiceProperty(serviceName, podman.DropInPaths)
90+
if err != nil {
91+
log.Debug().Err(err).Msgf("failed to get the path to the %s service configuration files", serviceName)
92+
} else {
93+
result = append(result, strings.Split(dropIns, " ")...)
9394
}
9495
}
9596
return result

mgradm/shared/templates/salineServiceTemplate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ ExecStart=/bin/sh -c '/usr/bin/podman run \
4242
{{- end }}
4343
-e TZ=${TZ} \
4444
-e NOSSL=YES \
45-
${PODMAN_EXTRA_ARGS} ${UYUNI_SALINE_IMAGE}'
45+
${UYUNI_SALINE_IMAGE}'
4646
ExecStop=/usr/bin/podman stop --ignore -t 10 --cidfile=%t/%n.ctr-id
4747
ExecStopPost=/usr/bin/podman rm -f --ignore -t 10 --cidfile=%t/%n.ctr-id
4848
PIDFile=%t/uyuni-saline.pid

shared/podman/images.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net/url"
1313
"os"
1414
"path"
15+
"path/filepath"
1516
"regexp"
1617
"strings"
1718

@@ -226,7 +227,7 @@ func loadRpmImage(rpmImageBasePath string) (string, error) {
226227
return "", fmt.Errorf(L("error parsing: %s"), string(out))
227228
}
228229

229-
// IsImagePresent return true if the image is present.
230+
// IsImagePresent returns the image name if the image is present.
230231
func IsImagePresent(image string) (string, error) {
231232
log.Debug().Msgf("Checking for %s", image)
232233
out, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "images", "--format={{ .Repository }}", image)
@@ -354,18 +355,21 @@ func DeleteImage(name string, dryRun bool) error {
354355
return nil
355356
}
356357

358+
var newRunner = utils.NewRunner
359+
357360
// ExportImage saves a podman image based on its name to a specified directory.
358361
// outputDir option expects already existing directory.
359362
// If dryRun is set to true, nothing will be done, only messages logged to explain what would happen.
360363
func ExportImage(name string, outputDir string, dryRun bool) error {
361364
exists := imageExists(name)
362365
if exists {
363-
saveCommand := []string{"podman", "image", "save", "--quiet", "-o", path.Join(outputDir, name+".tar"), name}
366+
baseName, _, _ := strings.Cut(filepath.Base(name), ":")
367+
saveCommand := []string{"podman", "image", "save", "--quiet", "-o", path.Join(outputDir, baseName+".tar"), name}
364368
if dryRun {
365369
log.Info().Msgf(L("Would run %s"), strings.Join(saveCommand, " "))
366370
} else {
367371
log.Info().Msgf(L("Run %s"), strings.Join(saveCommand, " "))
368-
err := utils.RunCmd(saveCommand[0], saveCommand[1:]...)
372+
_, err := newRunner(saveCommand[0], saveCommand[1:]...).Exec()
369373
if err != nil {
370374
return utils.Errorf(err, L("Failed to export image %s"), name)
371375
}
@@ -380,12 +384,12 @@ func imageExists(image string) bool {
380384
}
381385

382386
func RestoreImage(imageFile string, dryRun bool) error {
383-
restoreCommand := []string{"podman", "image", "import", "--quiet", imageFile}
387+
restoreCommand := []string{"podman", "image", "load", "--quiet", "-i", imageFile}
384388
if dryRun {
385389
log.Info().Msgf(L("Would run %s"), strings.Join(restoreCommand, " "))
386390
} else {
387391
log.Info().Msgf(L("Run %s"), strings.Join(restoreCommand, " "))
388-
err := utils.RunCmd(restoreCommand[0], restoreCommand[1:]...)
392+
_, err := newRunner(restoreCommand[0], restoreCommand[1:]...).Exec()
389393
if err != nil {
390394
return utils.Errorf(err, L("Failed to restore image %s"), imageFile)
391395
}

shared/podman/interfaces.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
// SPDX-FileCopyrightText: 2024 SUSE LLC
1+
// SPDX-FileCopyrightText: 2025 SUSE LLC
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

55
package podman
66

7+
const (
8+
// FragmentPath is a systemd property containing the path of the service file.
9+
FragmentPath = "FragmentPath"
10+
11+
// DropInPaths is a systemd property containing the paths of the service configuration file separated by a space.
12+
DropInPaths = "DropInPaths"
13+
)
14+
715
// Systemd is an interface providing systemd operations.
816
type Systemd interface {
917

@@ -65,4 +73,7 @@ type Systemd interface {
6573

6674
// GetServicesFromSystemdFiles return the uyuni enabled services as string list.
6775
GetServicesFromSystemdFiles(systemdFileList string) []string
76+
77+
// GetServiceProperty returns the value of a systemd service property.
78+
GetServiceProperty(service string, property string) (string, error)
6879
}

shared/podman/systemd.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ func GetServicePath(name string) string {
7373
return path.Join(servicesPath, name+".service")
7474
}
7575

76+
func (s SystemdImpl) GetServiceProperty(service string, property string) (string, error) {
77+
serviceName := service
78+
if strings.HasSuffix(service, "@") {
79+
serviceName = service + "0"
80+
}
81+
out, err := newRunner("systemctl", "show", "-p", property, serviceName).Exec()
82+
if err != nil {
83+
return "", utils.Errorf(err, L("Failed to get the %[1]s property from %[2]s service"), property, service)
84+
}
85+
return strings.TrimPrefix(strings.TrimSpace(string(out)), property+"="), nil
86+
}
87+
7688
// GetServiceConfFolder return the conf folder for systemd services.
7789
func GetServiceConfFolder(name string) string {
7890
return path.Join(servicesPath, name+".service.d")

shared/podman/systemd_test.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
// SPDX-FileCopyrightText: 2024 SUSE LLC
1+
// SPDX-FileCopyrightText: 2025 SUSE LLC
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

55
package podman
66

77
import (
8+
"errors"
89
"os"
910
"path"
11+
"strings"
1012
"testing"
1113

1214
"github.com/uyuni-project/uyuni-tools/shared/testutils"
@@ -91,3 +93,21 @@ Environment="PODMAN_EXTRA_ARGS="
9193
actual = testutils.ReadFile(t, path.Join(serviceConfDir, "custom.conf"))
9294
testutils.AssertEquals(t, "invalid custom.conf file", customFile, actual)
9395
}
96+
97+
func TestGetServiceProperty(t *testing.T) {
98+
newRunner = testutils.FakeRunnerGenerator("TestProperty=foo bar\n", nil)
99+
tested := SystemdImpl{}
100+
actual, err := tested.GetServiceProperty("myservice", "TestProperty")
101+
testutils.AssertTrue(t, "No error expected", err == nil)
102+
testutils.AssertEquals(t, "Wrong expected property", "foo bar", actual)
103+
}
104+
105+
func TestGetServicePropertyError(t *testing.T) {
106+
newRunner = testutils.FakeRunnerGenerator("", errors.New("Test error"))
107+
tested := SystemdImpl{}
108+
actual, err := tested.GetServiceProperty("myservice", "TestProperty")
109+
testutils.AssertTrue(t, "Error message missing the root error message", strings.Contains(err.Error(), "Test error"))
110+
testutils.AssertTrue(t, "Unexpected error description",
111+
strings.Contains(err.Error(), "Failed to get the TestProperty property from myservice service"))
112+
testutils.AssertEquals(t, "Wrong expected property", "", actual)
113+
}

shared/podman/utils.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,10 @@ func GetServiceImage(service string) string {
146146
return ""
147147
}
148148

149-
imageFinder := regexp.MustCompile(`UYUNI_IMAGE=(.*)`)
149+
imageFinder := regexp.MustCompile(`UYUNI.*_IMAGE=(.*)`)
150150
matches := imageFinder.FindStringSubmatch(string(out))
151151
if len(matches) < 2 {
152-
log.Warn().Msgf(L("no UYUNI_IMAGE defined in %s systemd service"), service)
152+
log.Warn().Msgf(L("no UYUNI.*_IMAGE defined in %s systemd service"), service)
153153
return ""
154154
}
155155
return matches[1]

shared/testutils/runner.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-FileCopyrightText: 2025 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package testutils
6+
7+
import (
8+
"github.com/rs/zerolog"
9+
"github.com/uyuni-project/uyuni-tools/shared/types"
10+
)
11+
12+
type fakeRunner struct {
13+
out []byte
14+
err error
15+
}
16+
17+
func (r fakeRunner) Log(_ zerolog.Level) types.Runner {
18+
return r
19+
}
20+
21+
func (r fakeRunner) Spinner(_ string) types.Runner {
22+
return r
23+
}
24+
25+
func (r fakeRunner) StdMapping() types.Runner {
26+
return r
27+
}
28+
29+
func (r fakeRunner) Env(_ []string) types.Runner {
30+
return r
31+
}
32+
33+
func (r fakeRunner) Exec() ([]byte, error) {
34+
return r.out, r.err
35+
}
36+
37+
// FakeRunnerGenerator creates NewRunner function generating a FakeRunner.
38+
// out and err are the returns of the mocked Exec().
39+
func FakeRunnerGenerator(out string, err error) func(string, ...string) types.Runner {
40+
return func(_ string, _ ...string) types.Runner {
41+
runner := fakeRunner{
42+
out: []byte(out),
43+
err: err,
44+
}
45+
return runner
46+
}
47+
}

shared/types/runner.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-FileCopyrightText: 2025 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package types
6+
7+
import "github.com/rs/zerolog"
8+
9+
// Runner is an interface to execute system calls.
10+
type Runner interface {
11+
// Log sets the log level of the output.
12+
Log(logLevel zerolog.Level) Runner
13+
14+
// Spinner sets a spinner with its message.
15+
// If no message is passed, the command will be used.
16+
Spinner(message string) Runner
17+
18+
// StdMapping maps the process output and error streams to the standard ones.
19+
// This is useful to show the process output in the console and the logs and can be combined with Log().
20+
StdMapping() Runner
21+
22+
// Env sets environment variables to use for the command.
23+
Env(env []string) Runner
24+
25+
// Exec really executes the command and returns its output and error.
26+
// The error output to used as error message if the StdMapping() function wasn't called.
27+
Exec() ([]byte, error)
28+
}

0 commit comments

Comments
 (0)