github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/osutil/mountentry_linux_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package osutil_test
    21  
    22  import (
    23  	"math"
    24  	"os"
    25  	"syscall"
    26  
    27  	. "gopkg.in/check.v1"
    28  
    29  	"github.com/snapcore/snapd/osutil"
    30  )
    31  
    32  type entrySuite struct{}
    33  
    34  var _ = Suite(&entrySuite{})
    35  
    36  func (s *entrySuite) TestString(c *C) {
    37  	ent0 := osutil.MountEntry{}
    38  	c.Assert(ent0.String(), Equals, "none none none defaults 0 0")
    39  	ent1 := osutil.MountEntry{
    40  		Name:    "/var/snap/foo/common",
    41  		Dir:     "/var/snap/bar/common",
    42  		Options: []string{"bind"},
    43  	}
    44  	c.Assert(ent1.String(), Equals,
    45  		"/var/snap/foo/common /var/snap/bar/common none bind 0 0")
    46  	ent2 := osutil.MountEntry{
    47  		Name:    "/dev/sda5",
    48  		Dir:     "/media/foo",
    49  		Type:    "ext4",
    50  		Options: []string{"rw,noatime"},
    51  	}
    52  	c.Assert(ent2.String(), Equals, "/dev/sda5 /media/foo ext4 rw,noatime 0 0")
    53  	ent3 := osutil.MountEntry{
    54  		Name:    "/dev/sda5",
    55  		Dir:     "/media/My Files",
    56  		Type:    "ext4",
    57  		Options: []string{"rw,noatime"},
    58  	}
    59  	c.Assert(ent3.String(), Equals, `/dev/sda5 /media/My\040Files ext4 rw,noatime 0 0`)
    60  }
    61  
    62  func (s *entrySuite) TestEqual(c *C) {
    63  	var a, b *osutil.MountEntry
    64  	a = &osutil.MountEntry{}
    65  	b = &osutil.MountEntry{}
    66  	c.Assert(a.Equal(b), Equals, true)
    67  	a = &osutil.MountEntry{Dir: "foo"}
    68  	b = &osutil.MountEntry{Dir: "foo"}
    69  	c.Assert(a.Equal(b), Equals, true)
    70  	a = &osutil.MountEntry{Options: []string{"ro"}}
    71  	b = &osutil.MountEntry{Options: []string{"ro"}}
    72  	c.Assert(a.Equal(b), Equals, true)
    73  	a = &osutil.MountEntry{Dir: "foo"}
    74  	b = &osutil.MountEntry{Dir: "bar"}
    75  	c.Assert(a.Equal(b), Equals, false)
    76  	a = &osutil.MountEntry{}
    77  	b = &osutil.MountEntry{Options: []string{"ro"}}
    78  	c.Assert(a.Equal(b), Equals, false)
    79  	a = &osutil.MountEntry{Options: []string{"ro"}}
    80  	b = &osutil.MountEntry{Options: []string{"rw"}}
    81  	c.Assert(a.Equal(b), Equals, false)
    82  }
    83  
    84  // Test that typical fstab entry is parsed correctly.
    85  func (s *entrySuite) TestParseMountEntry1(c *C) {
    86  	e, err := osutil.ParseMountEntry("UUID=394f32c0-1f94-4005-9717-f9ab4a4b570b /               ext4    errors=remount-ro 0       1")
    87  	c.Assert(err, IsNil)
    88  	c.Assert(e.Name, Equals, "UUID=394f32c0-1f94-4005-9717-f9ab4a4b570b")
    89  	c.Assert(e.Dir, Equals, "/")
    90  	c.Assert(e.Type, Equals, "ext4")
    91  	c.Assert(e.Options, DeepEquals, []string{"errors=remount-ro"})
    92  	c.Assert(e.DumpFrequency, Equals, 0)
    93  	c.Assert(e.CheckPassNumber, Equals, 1)
    94  
    95  	e, err = osutil.ParseMountEntry("none /tmp tmpfs")
    96  	c.Assert(err, IsNil)
    97  	c.Assert(e.Name, Equals, "none")
    98  	c.Assert(e.Dir, Equals, "/tmp")
    99  	c.Assert(e.Type, Equals, "tmpfs")
   100  	c.Assert(e.Options, IsNil)
   101  	c.Assert(e.DumpFrequency, Equals, 0)
   102  	c.Assert(e.CheckPassNumber, Equals, 0)
   103  }
   104  
   105  // Test that hash inside a field value is supported.
   106  func (s *entrySuite) TestHashInFieldValue(c *C) {
   107  	e, err := osutil.ParseMountEntry("mhddfs#/mnt/dir1,/mnt/dir2 /mnt/dir fuse defaults,allow_other 0 0")
   108  	c.Assert(err, IsNil)
   109  	c.Assert(e.Name, Equals, "mhddfs#/mnt/dir1,/mnt/dir2")
   110  	c.Assert(e.Dir, Equals, "/mnt/dir")
   111  	c.Assert(e.Type, Equals, "fuse")
   112  	c.Assert(e.Options, DeepEquals, []string{"defaults", "allow_other"})
   113  	c.Assert(e.DumpFrequency, Equals, 0)
   114  	c.Assert(e.CheckPassNumber, Equals, 0)
   115  }
   116  
   117  // Test that options are parsed correctly
   118  func (s *entrySuite) TestParseMountEntry2(c *C) {
   119  	e, err := osutil.ParseMountEntry("name dir type options,comma,separated 0 0")
   120  	c.Assert(err, IsNil)
   121  	c.Assert(e.Name, Equals, "name")
   122  	c.Assert(e.Dir, Equals, "dir")
   123  	c.Assert(e.Type, Equals, "type")
   124  	c.Assert(e.Options, DeepEquals, []string{"options", "comma", "separated"})
   125  	c.Assert(e.DumpFrequency, Equals, 0)
   126  	c.Assert(e.CheckPassNumber, Equals, 0)
   127  }
   128  
   129  // Test that whitespace escape codes are honored
   130  func (s *entrySuite) TestParseMountEntry3(c *C) {
   131  	e, err := osutil.ParseMountEntry(`na\040me d\011ir ty\012pe optio\134ns 0 0`)
   132  	c.Assert(err, IsNil)
   133  	c.Assert(e.Name, Equals, "na me")
   134  	c.Assert(e.Dir, Equals, "d\tir")
   135  	c.Assert(e.Type, Equals, "ty\npe")
   136  	c.Assert(e.Options, DeepEquals, []string{`optio\ns`})
   137  	c.Assert(e.DumpFrequency, Equals, 0)
   138  	c.Assert(e.CheckPassNumber, Equals, 0)
   139  }
   140  
   141  // Test that number of fields is checked
   142  func (s *entrySuite) TestParseMountEntry4(c *C) {
   143  	for _, s := range []string{
   144  		"", "1", "1 2" /* skip 3, 4, 5 and 6 fields (valid case) */, "1 2 3 4 5 6 7",
   145  	} {
   146  		_, err := osutil.ParseMountEntry(s)
   147  		c.Assert(err, ErrorMatches, "expected between 3 and 6 fields, found [01237]")
   148  	}
   149  }
   150  
   151  // Test that integers are parsed and error checked
   152  func (s *entrySuite) TestParseMountEntry5(c *C) {
   153  	_, err := osutil.ParseMountEntry("name dir type options foo 0")
   154  	c.Assert(err, ErrorMatches, "cannot parse dump frequency: .*")
   155  	_, err = osutil.ParseMountEntry("name dir type options 0 foo")
   156  	c.Assert(err, ErrorMatches, "cannot parse check pass number: .*")
   157  }
   158  
   159  // Test that last two integer fields default to zero if not present.
   160  func (s *entrySuite) TestParseMountEntry6(c *C) {
   161  	e, err := osutil.ParseMountEntry("name dir type options")
   162  	c.Assert(err, IsNil)
   163  	c.Assert(e.DumpFrequency, Equals, 0)
   164  	c.Assert(e.CheckPassNumber, Equals, 0)
   165  
   166  	e, err = osutil.ParseMountEntry("name dir type options 5")
   167  	c.Assert(err, IsNil)
   168  	c.Assert(e.DumpFrequency, Equals, 5)
   169  	c.Assert(e.CheckPassNumber, Equals, 0)
   170  
   171  	e, err = osutil.ParseMountEntry("name dir type options 5 7")
   172  	c.Assert(err, IsNil)
   173  	c.Assert(e.DumpFrequency, Equals, 5)
   174  	c.Assert(e.CheckPassNumber, Equals, 7)
   175  }
   176  
   177  // Test (string) options -> (int) flag conversion code.
   178  func (s *entrySuite) TestMountOptsToFlags(c *C) {
   179  	flags, err := osutil.MountOptsToFlags(nil)
   180  	c.Assert(err, IsNil)
   181  	c.Assert(flags, Equals, 0)
   182  	flags, err = osutil.MountOptsToFlags([]string{"ro", "nodev", "nosuid"})
   183  	c.Assert(err, IsNil)
   184  	c.Assert(flags, Equals, syscall.MS_RDONLY|syscall.MS_NODEV|syscall.MS_NOSUID)
   185  	_, err = osutil.MountOptsToFlags([]string{"bogus"})
   186  	c.Assert(err, ErrorMatches, `unsupported mount option: "bogus"`)
   187  	// The x-snapd-prefix is reserved for non-kernel parameters that do not
   188  	// translate to kernel level mount flags. This is similar to systemd or
   189  	// udisks that use fstab options to convey additional data.
   190  	flags, err = osutil.MountOptsToFlags([]string{"x-snapd.foo"})
   191  	c.Assert(err, IsNil)
   192  	c.Assert(flags, Equals, 0)
   193  }
   194  
   195  // Test (string) options -> (int, unparsed) flag conversion code.
   196  func (s *entrySuite) TestMountOptsToCommonFlags(c *C) {
   197  	flags, unparsed := osutil.MountOptsToCommonFlags(nil)
   198  	c.Assert(flags, Equals, 0)
   199  	c.Assert(unparsed, HasLen, 0)
   200  	flags, unparsed = osutil.MountOptsToCommonFlags([]string{"ro", "nodev", "nosuid"})
   201  	c.Assert(flags, Equals, syscall.MS_RDONLY|syscall.MS_NODEV|syscall.MS_NOSUID)
   202  	c.Assert(unparsed, HasLen, 0)
   203  	flags, unparsed = osutil.MountOptsToCommonFlags([]string{"bogus"})
   204  	c.Assert(flags, Equals, 0)
   205  	c.Assert(unparsed, DeepEquals, []string{"bogus"})
   206  	// The x-snapd-prefix is reserved for non-kernel parameters that do not
   207  	// translate to kernel level mount flags. This is similar to systemd or
   208  	// udisks that use fstab options to convey additional data. Those are not
   209  	// returned as "unparsed" as we don't want to pass them to the kernel.
   210  	flags, unparsed = osutil.MountOptsToCommonFlags([]string{"x-snapd.foo"})
   211  	c.Assert(flags, Equals, 0)
   212  	c.Assert(unparsed, HasLen, 0)
   213  	// The "rw" flag is recognized but doesn't translate to an actual value
   214  	// since read-write is the implicit default and there are no kernel level
   215  	// flags to express it.
   216  	flags, unparsed = osutil.MountOptsToCommonFlags([]string{"rw"})
   217  	c.Assert(flags, Equals, 0)
   218  	c.Assert(unparsed, DeepEquals, []string(nil))
   219  }
   220  
   221  func (s *entrySuite) TestOptStr(c *C) {
   222  	e := &osutil.MountEntry{Options: []string{"key=value"}}
   223  	val, ok := e.OptStr("key")
   224  	c.Assert(ok, Equals, true)
   225  	c.Assert(val, Equals, "value")
   226  
   227  	val, ok = e.OptStr("missing")
   228  	c.Assert(ok, Equals, false)
   229  	c.Assert(val, Equals, "")
   230  }
   231  
   232  func (s *entrySuite) TestOptBool(c *C) {
   233  	e := &osutil.MountEntry{Options: []string{"key"}}
   234  	val := e.OptBool("key")
   235  	c.Assert(val, Equals, true)
   236  
   237  	val = e.OptBool("missing")
   238  	c.Assert(val, Equals, false)
   239  }
   240  
   241  func (s *entrySuite) TestOptionHelpers(c *C) {
   242  	c.Assert(osutil.XSnapdUser(1000), Equals, "x-snapd.user=1000")
   243  	c.Assert(osutil.XSnapdGroup(1000), Equals, "x-snapd.group=1000")
   244  	c.Assert(osutil.XSnapdMode(0755), Equals, "x-snapd.mode=0755")
   245  	c.Assert(osutil.XSnapdSymlink("oldname"), Equals, "x-snapd.symlink=oldname")
   246  }
   247  
   248  func (s *entrySuite) TestXSnapdMode(c *C) {
   249  	// Mode has a default value.
   250  	e := &osutil.MountEntry{}
   251  	mode, err := e.XSnapdMode()
   252  	c.Assert(err, IsNil)
   253  	c.Assert(mode, Equals, os.FileMode(0755))
   254  
   255  	// Mode is parsed from the x-snapd.mode= option.
   256  	e = &osutil.MountEntry{Options: []string{"x-snapd.mode=0700"}}
   257  	mode, err = e.XSnapdMode()
   258  	c.Assert(err, IsNil)
   259  	c.Assert(mode, Equals, os.FileMode(0700))
   260  
   261  	// Empty value is invalid.
   262  	e = &osutil.MountEntry{Options: []string{"x-snapd.mode="}}
   263  	_, err = e.XSnapdMode()
   264  	c.Assert(err, ErrorMatches, `cannot parse octal file mode from ""`)
   265  
   266  	// As well as other bogus values.
   267  	e = &osutil.MountEntry{Options: []string{"x-snapd.mode=pasta"}}
   268  	_, err = e.XSnapdMode()
   269  	c.Assert(err, ErrorMatches, `cannot parse octal file mode from "pasta"`)
   270  
   271  	// And even valid values with trailing garbage.
   272  	e = &osutil.MountEntry{Options: []string{"x-snapd.mode=0700pasta"}}
   273  	mode, err = e.XSnapdMode()
   274  	c.Assert(err, ErrorMatches, `cannot parse octal file mode from "0700pasta"`)
   275  	c.Assert(mode, Equals, os.FileMode(0))
   276  }
   277  
   278  func (s *entrySuite) TestXSnapdUID(c *C) {
   279  	// User has a default value.
   280  	e := &osutil.MountEntry{}
   281  	uid, err := e.XSnapdUID()
   282  	c.Assert(err, IsNil)
   283  	c.Assert(uid, Equals, uint64(0))
   284  
   285  	// User is parsed from the x-snapd.uid= option.
   286  	e = &osutil.MountEntry{Options: []string{"x-snapd.uid=root"}}
   287  	uid, err = e.XSnapdUID()
   288  	c.Assert(err, ErrorMatches, `cannot parse user name "root"`)
   289  	c.Assert(uid, Equals, uint64(math.MaxUint64))
   290  
   291  	// Numeric names are used as-is.
   292  	e = &osutil.MountEntry{Options: []string{"x-snapd.uid=123"}}
   293  	uid, err = e.XSnapdUID()
   294  	c.Assert(err, IsNil)
   295  	c.Assert(uid, Equals, uint64(123))
   296  
   297  	// And even valid values with trailing garbage.
   298  	e = &osutil.MountEntry{Options: []string{"x-snapd.uid=0bogus"}}
   299  	uid, err = e.XSnapdUID()
   300  	c.Assert(err, ErrorMatches, `cannot parse user name "0bogus"`)
   301  	c.Assert(uid, Equals, uint64(math.MaxUint64))
   302  }
   303  
   304  func (s *entrySuite) TestXSnapdGID(c *C) {
   305  	// Group has a default value.
   306  	e := &osutil.MountEntry{}
   307  	gid, err := e.XSnapdGID()
   308  	c.Assert(err, IsNil)
   309  	c.Assert(gid, Equals, uint64(0))
   310  
   311  	e = &osutil.MountEntry{Options: []string{"x-snapd.gid=root"}}
   312  	gid, err = e.XSnapdGID()
   313  	c.Assert(err, ErrorMatches, `cannot parse group name "root"`)
   314  	c.Assert(gid, Equals, uint64(math.MaxUint64))
   315  
   316  	// Numeric names are used as-is.
   317  	e = &osutil.MountEntry{Options: []string{"x-snapd.gid=456"}}
   318  	gid, err = e.XSnapdGID()
   319  	c.Assert(err, IsNil)
   320  	c.Assert(gid, Equals, uint64(456))
   321  
   322  	// And even valid values with trailing garbage.
   323  	e = &osutil.MountEntry{Options: []string{"x-snapd.gid=0bogus"}}
   324  	gid, err = e.XSnapdGID()
   325  	c.Assert(err, ErrorMatches, `cannot parse group name "0bogus"`)
   326  	c.Assert(gid, Equals, uint64(math.MaxUint64))
   327  }
   328  
   329  func (s *entrySuite) TestXSnapdEntryID(c *C) {
   330  	// Entry ID is optional and defaults to the mount point.
   331  	e := &osutil.MountEntry{Dir: "/foo"}
   332  	c.Assert(e.XSnapdEntryID(), Equals, "/foo")
   333  
   334  	// Entry ID is parsed from the x-snapd.id= option.
   335  	e = &osutil.MountEntry{Dir: "/foo", Options: []string{"x-snapd.id=foo"}}
   336  	c.Assert(e.XSnapdEntryID(), Equals, "foo")
   337  }
   338  
   339  func (s *entrySuite) TestXSnapdNeededBy(c *C) {
   340  	// The needed-by attribute is optional.
   341  	e := &osutil.MountEntry{}
   342  	c.Assert(e.XSnapdNeededBy(), Equals, "")
   343  
   344  	// The needed-by attribute parsed from the x-snapd.needed-by= option.
   345  	e = &osutil.MountEntry{Options: []string{"x-snap.id=foo", "x-snapd.needed-by=bar"}}
   346  	c.Assert(e.XSnapdNeededBy(), Equals, "bar")
   347  
   348  	// There's a helper function that returns this option string.
   349  	c.Assert(osutil.XSnapdNeededBy("foo"), Equals, "x-snapd.needed-by=foo")
   350  }
   351  
   352  func (s *entrySuite) TestXSnapdSynthetic(c *C) {
   353  	// Entries are not synthetic unless tagged as such.
   354  	e := &osutil.MountEntry{}
   355  	c.Assert(e.XSnapdSynthetic(), Equals, false)
   356  
   357  	// Tagging is done with x-snapd.synthetic option.
   358  	e = &osutil.MountEntry{Options: []string{"x-snapd.synthetic"}}
   359  	c.Assert(e.XSnapdSynthetic(), Equals, true)
   360  
   361  	// There's a helper function that returns this option string.
   362  	c.Assert(osutil.XSnapdSynthetic(), Equals, "x-snapd.synthetic")
   363  }
   364  
   365  func (s *entrySuite) TestXSnapdOrigin(c *C) {
   366  	// Entries have no origin by default.
   367  	e := &osutil.MountEntry{}
   368  	c.Assert(e.XSnapdOrigin(), Equals, "")
   369  
   370  	// Origin can be indicated with the x-snapd.origin= option.
   371  	e = &osutil.MountEntry{Options: []string{osutil.XSnapdOriginLayout()}}
   372  	c.Assert(e.XSnapdOrigin(), Equals, "layout")
   373  
   374  	// There's a helper function that returns this option string.
   375  	c.Assert(osutil.XSnapdOriginLayout(), Equals, "x-snapd.origin=layout")
   376  
   377  	// Origin can also indicate a parallel snap instance setup
   378  	e = &osutil.MountEntry{Options: []string{osutil.XSnapdOriginOvername()}}
   379  	c.Assert(e.XSnapdOrigin(), Equals, "overname")
   380  	c.Assert(osutil.XSnapdOriginOvername(), Equals, "x-snapd.origin=overname")
   381  }
   382  
   383  func (s *entrySuite) TestXSnapdDetach(c *C) {
   384  	// Entries are not detached by default.
   385  	e := &osutil.MountEntry{}
   386  	c.Assert(e.XSnapdDetach(), Equals, false)
   387  
   388  	// Detach can be requested with the x-snapd.detach option.
   389  	e = &osutil.MountEntry{Options: []string{osutil.XSnapdDetach()}}
   390  	c.Assert(e.XSnapdDetach(), Equals, true)
   391  
   392  	// There's a helper function that returns this option string.
   393  	c.Assert(osutil.XSnapdDetach(), Equals, "x-snapd.detach")
   394  }
   395  
   396  func (s *entrySuite) TestXSnapdKind(c *C) {
   397  	// Entries have a kind (directory, file or symlink). Directory is spelled
   398  	// as an empty string though, for backwards compatibility.
   399  	e := &osutil.MountEntry{}
   400  	c.Assert(e.XSnapdKind(), Equals, "")
   401  
   402  	// A bind mount entry can refer to a file using the x-snapd.kind=file option string.
   403  	e = &osutil.MountEntry{Options: []string{osutil.XSnapdKindFile()}}
   404  	c.Assert(e.XSnapdKind(), Equals, "file")
   405  
   406  	// There's a helper function that returns this option string.
   407  	c.Assert(osutil.XSnapdKindFile(), Equals, "x-snapd.kind=file")
   408  
   409  	// A mount entry can create a symlink by using the x-snapd.kind=symlink option string.
   410  	e = &osutil.MountEntry{Options: []string{osutil.XSnapdKindSymlink()}}
   411  	c.Assert(e.XSnapdKind(), Equals, "symlink")
   412  
   413  	// There's a helper function that returns this option string.
   414  	c.Assert(osutil.XSnapdKindSymlink(), Equals, "x-snapd.kind=symlink")
   415  }
   416  
   417  func (s *entrySuite) TestXSnapdSymlink(c *C) {
   418  	// Entries without the x-snapd.symlink key return an empty string
   419  	e := &osutil.MountEntry{}
   420  	c.Assert(e.XSnapdSymlink(), Equals, "")
   421  
   422  	// A mount entry can list a symlink target
   423  	e = &osutil.MountEntry{Options: []string{osutil.XSnapdSymlink("target")}}
   424  	c.Assert(e.XSnapdSymlink(), Equals, "target")
   425  }
   426  
   427  func (s *entrySuite) TestXSnapdIgnoreMissing(c *C) {
   428  	// By default entries will not have the ignore missing flag set
   429  	e := &osutil.MountEntry{}
   430  	c.Assert(e.XSnapdIgnoreMissing(), Equals, false)
   431  
   432  	// A mount entry can specify that it should be ignored if the
   433  	// mount source or target are missing with the
   434  	// x-snapd.ignore-missing option.
   435  	e = &osutil.MountEntry{Options: []string{osutil.XSnapdIgnoreMissing()}}
   436  	c.Assert(e.XSnapdIgnoreMissing(), Equals, true)
   437  
   438  	// There's a helper function that returns this option string.
   439  	c.Assert(osutil.XSnapdIgnoreMissing(), Equals, "x-snapd.ignore-missing")
   440  }