systemd: add a method to list mount units created by a snap

This commit is contained in:
Alberto Mardegan
2021-08-19 17:52:27 +03:00
parent 7d86a7d247
commit ff307c1d85
3 changed files with 184 additions and 0 deletions

View File

@@ -192,6 +192,10 @@ func (s *emulation) RemoveMountUnitFile(mountedDir string) error {
return nil
}
func (s *emulation) ListMountUnits(snapName, origin string) ([]string, error) {
return nil, errNotImplemented
}
func (s *emulation) Mask(service string) error {
_, err := systemctlCmd("--root", s.rootDir, "mask", service)
return err

View File

@@ -280,6 +280,9 @@ type Systemd interface {
AddMountUnitFileWithOptions(unitOptions *MountUnitOptions) (string, error)
// RemoveMountUnitFile unmounts/stops/disables/removes a mount unit.
RemoveMountUnitFile(baseDir string) error
// ListMountUnits gets the list of targets of the mount units created by
// the `origin` module for the given snap
ListMountUnits(snapName, origin string) ([]string, error)
// Mask the given service.
Mask(service string) error
// Unmask the given service.
@@ -1275,6 +1278,101 @@ func (s *systemd) RemoveMountUnitFile(mountedDir string) error {
return nil
}
func workaroundSystemdQuoting(fragmentPath, where string) string {
// We know that the directory components of the fragment path do not need
// quoting and are therefore reliable. As for the file name, we workaround
// the wrong quoting of older systemd version by re-encoding the "Where"
// ourselves.
dir := filepath.Dir(fragmentPath)
baseName := EscapeUnitNamePath(where)
unitType := filepath.Ext(fragmentPath)
return filepath.Join(dir, baseName+unitType)
}
func extractOriginModule(systemdUnitPath string) (string, error) {
f, err := os.Open(systemdUnitPath)
if err != nil {
return "", err
}
defer f.Close()
var originModule string
s := bufio.NewScanner(f)
prefix := snappyOriginModule + "="
for s.Scan() {
line := s.Text()
if strings.HasPrefix(line, prefix) {
originModule = line[len(prefix):]
break
}
}
return originModule, nil
}
func (s *systemd) ListMountUnits(snapName, origin string) ([]string, error) {
out, err := s.systemctl("show", "--property=Description,Where,FragmentPath", "*.mount")
if err != nil {
return nil, err
}
var mountPoints []string
if bytes.TrimSpace(out) == nil {
return mountPoints, nil
}
// Results are separated by a blank line, so we can split them like this:
units := bytes.Split(out, []byte("\n\n"))
for _, unitOutput := range units {
var where, description, fragmentPath string
lines := bytes.Split(bytes.Trim(unitOutput, "\n"), []byte("\n"))
for _, line := range lines {
splitVal := strings.SplitN(string(line), "=", 2)
if len(splitVal) != 2 {
return nil, fmt.Errorf("cannot parse systemctl output: %q", line)
}
switch splitVal[0] {
case "Description":
description = splitVal[1]
case "Where":
where = splitVal[1]
case "FragmentPath":
fragmentPath = splitVal[1]
default:
return nil, fmt.Errorf("unexpected property %q", splitVal[0])
}
}
ourDescription := fmt.Sprintf("Mount unit for %s", snapName)
if !strings.HasPrefix(description, ourDescription) {
continue
}
// Under Ubuntu 16.04, systemd improperly quotes the FragmentPath, so
// we must do some extra work here to get the correct path. This code
// can be removed once we stop supporting old distros
fragmentPath = workaroundSystemdQuoting(fragmentPath, where)
// only return units programmatically created by some snapd backend:
// the mount unit used to mount the snap's squashfs is generally
// uninteresting
originModule, err := extractOriginModule(fragmentPath)
if err != nil || originModule == "" {
continue
}
// If an `origin` was given, we must return only units created by it
if origin != "" && originModule != origin {
continue
}
if where == "" {
return nil, fmt.Errorf(`missing "Where" in mount unit %q`, fragmentPath)
}
mountPoints = append(mountPoints, where)
}
return mountPoints, nil
}
func (s *systemd) ReloadOrRestart(serviceName string) error {
if s.mode == GlobalUserMode {
panic("cannot call restart with GlobalUserMode")

View File

@@ -1405,6 +1405,88 @@ func (s *SystemdTestSuite) TestUnmaskInEmulationMode(c *C) {
{"--root", "/path", "unmask", "foo"}})
}
func (s *SystemdTestSuite) TestListMountUnitsEmpty(c *C) {
s.outs = [][]byte{
[]byte("\n"),
}
sysd := New(SystemMode, nil)
units, err := sysd.ListMountUnits("some-snap", "")
c.Check(units, HasLen, 0)
c.Check(err, IsNil)
}
func (s *SystemdTestSuite) TestListMountUnitsMalformed(c *C) {
s.outs = [][]byte{
[]byte(`Description=Mount unit for some-snap, revision x1
Where=/somewhere/here
FragmentPath=/etc/systemd/system/somewhere-here.mount
HereIsOneLineWithoutAnEqualSign
`),
}
sysd := New(SystemMode, nil)
units, err := sysd.ListMountUnits("some-snap", "")
c.Check(units, HasLen, 0)
c.Check(err, ErrorMatches, "cannot parse systemctl output:.*")
}
func (s *SystemdTestSuite) TestListMountUnitsHappy(c *C) {
tmpDir, err := ioutil.TempDir("/tmp", "snapd-systemd-test-list-mounts-*")
c.Assert(err, IsNil)
defer os.RemoveAll(tmpDir)
var systemctlOutput string
createFakeUnit := func(fileName, snapName, where, origin string) error {
path := filepath.Join(tmpDir, fileName)
if len(systemctlOutput) > 0 {
systemctlOutput += "\n\n"
}
systemctlOutput += fmt.Sprintf(`Description=Mount unit for %s, revision x1
Where=%s
FragmentPath=%s
`, snapName, where, path)
contents := fmt.Sprintf(`[Unit]
Description=Mount unit for %s, revision x1
[Mount]
What=/does/not/matter
Where=%s
Type=doesntmatter
Options=do,not,matter,either
[Install]
WantedBy=multi-user.target
X-SnapdOrigin=%s
`, snapName, where, origin)
return ioutil.WriteFile(path, []byte(contents), 0644)
}
// Prepare the unit files
err = createFakeUnit("somepath-somedir.mount", "some-snap", "/somepath/somedir", "module1")
c.Assert(err, IsNil)
err = createFakeUnit("somewhere-here.mount", "some-other-snap", "/somewhere/here", "module2")
c.Assert(err, IsNil)
err = createFakeUnit("somewhere-there.mount", "some-snap", "/somewhere/there", "module3")
c.Assert(err, IsNil)
s.outs = [][]byte{
[]byte(systemctlOutput),
}
sysd := New(SystemMode, nil)
// First, get all mount units for some-snap, without filter on the origin module
units, err := sysd.ListMountUnits("some-snap", "")
c.Check(units, DeepEquals, []string{"/somepath/somedir", "/somewhere/there"})
c.Check(err, IsNil)
// Now repeat the same, filtering on the origin module
s.i = 0 // this resets the systemctl output iterator back to the beginning
units, err = sysd.ListMountUnits("some-snap", "module3")
c.Check(units, DeepEquals, []string{"/somewhere/there"})
c.Check(err, IsNil)
}
func (s *SystemdTestSuite) TestMountHappy(c *C) {
sysd := New(SystemMode, nil)