From ff307c1d85268e258802aaa84ee98d481e600ca2 Mon Sep 17 00:00:00 2001 From: Alberto Mardegan Date: Thu, 19 Aug 2021 17:52:27 +0300 Subject: [PATCH] systemd: add a method to list mount units created by a snap --- systemd/emulation.go | 4 ++ systemd/systemd.go | 98 +++++++++++++++++++++++++++++++++++++++++ systemd/systemd_test.go | 82 ++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+) diff --git a/systemd/emulation.go b/systemd/emulation.go index 9a0e6d3bfe..77fdf1dd04 100644 --- a/systemd/emulation.go +++ b/systemd/emulation.go @@ -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 diff --git a/systemd/systemd.go b/systemd/systemd.go index 434a270217..c15af1a8cb 100644 --- a/systemd/systemd.go +++ b/systemd/systemd.go @@ -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") diff --git a/systemd/systemd_test.go b/systemd/systemd_test.go index 24b9bac9ab..1f3c6878d8 100644 --- a/systemd/systemd_test.go +++ b/systemd/systemd_test.go @@ -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)