github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/interfaces/udev/backend_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2018 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 udev_test
    21  
    22  import (
    23  	"bytes"
    24  	"os"
    25  	"path/filepath"
    26  
    27  	. "gopkg.in/check.v1"
    28  
    29  	"github.com/snapcore/snapd/dirs"
    30  	"github.com/snapcore/snapd/interfaces"
    31  	"github.com/snapcore/snapd/interfaces/ifacetest"
    32  	"github.com/snapcore/snapd/interfaces/udev"
    33  	"github.com/snapcore/snapd/sandbox/cgroup"
    34  	"github.com/snapcore/snapd/snap"
    35  	"github.com/snapcore/snapd/testutil"
    36  	"github.com/snapcore/snapd/timings"
    37  )
    38  
    39  type backendSuite struct {
    40  	ifacetest.BackendSuite
    41  
    42  	udevadmCmd *testutil.MockCmd
    43  	meas       *timings.Span
    44  }
    45  
    46  var _ = Suite(&backendSuite{})
    47  
    48  var testedConfinementOpts = []interfaces.ConfinementOptions{
    49  	{},
    50  	{DevMode: true},
    51  	{JailMode: true},
    52  	{Classic: true},
    53  }
    54  
    55  func createSnippetForApps(apps map[string]*snap.AppInfo) string {
    56  	var buffer bytes.Buffer
    57  	for appName := range apps {
    58  		buffer.WriteString(appName)
    59  	}
    60  	return buffer.String()
    61  }
    62  
    63  func (s *backendSuite) SetUpTest(c *C) {
    64  	s.Backend = &udev.Backend{}
    65  
    66  	s.BackendSuite.SetUpTest(c)
    67  	c.Assert(s.Repo.AddBackend(s.Backend), IsNil)
    68  
    69  	// Mock away any real udev interaction
    70  	s.udevadmCmd = testutil.MockCommand(c, "udevadm", "")
    71  	// Prepare a directory for udev rules
    72  	// NOTE: Normally this is a part of the OS snap.
    73  	err := os.MkdirAll(dirs.SnapUdevRulesDir, 0700)
    74  	c.Assert(err, IsNil)
    75  
    76  	perf := timings.New(nil)
    77  	s.meas = perf.StartSpan("", "")
    78  }
    79  
    80  func (s *backendSuite) TearDownTest(c *C) {
    81  	s.udevadmCmd.Restore()
    82  
    83  	s.BackendSuite.TearDownTest(c)
    84  }
    85  
    86  // Tests for Setup() and Remove()
    87  func (s *backendSuite) TestName(c *C) {
    88  	c.Check(s.Backend.Name(), Equals, interfaces.SecurityUDev)
    89  }
    90  
    91  func (s *backendSuite) TestInstallingSnapWritesAndLoadsRules(c *C) {
    92  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
    93  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
    94  		spec.AddSnippet("dummy")
    95  		return nil
    96  	}
    97  	for _, opts := range testedConfinementOpts {
    98  		s.udevadmCmd.ForgetCalls()
    99  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   100  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   101  		// file called "70-snap.sambda.rules" was created
   102  		_, err := os.Stat(fname)
   103  		c.Check(err, IsNil)
   104  		// udevadm was used to reload rules and re-run triggers
   105  		c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
   106  			{"udevadm", "control", "--reload-rules"},
   107  			{"udevadm", "trigger", "--subsystem-nomatch=input"},
   108  			// FIXME: temporary until spec.TriggerSubsystem() can
   109  			// be called during disconnect
   110  			{"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"},
   111  			{"udevadm", "settle", "--timeout=10"},
   112  		})
   113  		s.RemoveSnap(c, snapInfo)
   114  	}
   115  }
   116  
   117  func (s *backendSuite) TestInstallingSnapWithHookWritesAndLoadsRules(c *C) {
   118  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   119  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   120  		spec.AddSnippet("dummy")
   121  		return nil
   122  	}
   123  	s.Iface.UDevPermanentPlugCallback = func(spec *udev.Specification, slot *snap.PlugInfo) error {
   124  		spec.AddSnippet("dummy")
   125  		return nil
   126  	}
   127  	for _, opts := range testedConfinementOpts {
   128  		s.udevadmCmd.ForgetCalls()
   129  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.HookYaml, 0)
   130  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.foo.rules")
   131  
   132  		// Verify that "70-snap.foo.rules" was created.
   133  		_, err := os.Stat(fname)
   134  		c.Check(err, IsNil)
   135  
   136  		// Verify that udevadm was used to reload rules and re-run triggers.
   137  		c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
   138  			{"udevadm", "control", "--reload-rules"},
   139  			{"udevadm", "trigger", "--subsystem-nomatch=input"},
   140  			// FIXME: temporary until spec.TriggerSubsystem() can
   141  			// be called during disconnect
   142  			{"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"},
   143  			{"udevadm", "settle", "--timeout=10"},
   144  		})
   145  		s.RemoveSnap(c, snapInfo)
   146  	}
   147  }
   148  
   149  func (s *backendSuite) TestSecurityIsStable(c *C) {
   150  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   151  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   152  		spec.AddSnippet("dummy")
   153  		return nil
   154  	}
   155  	for _, opts := range testedConfinementOpts {
   156  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   157  		s.udevadmCmd.ForgetCalls()
   158  		err := s.Backend.Setup(snapInfo, opts, s.Repo, s.meas)
   159  		c.Assert(err, IsNil)
   160  		// rules are not re-loaded when nothing changes
   161  		c.Check(s.udevadmCmd.Calls(), HasLen, 0)
   162  		s.RemoveSnap(c, snapInfo)
   163  	}
   164  }
   165  
   166  func (s *backendSuite) TestRemovingSnapRemovesAndReloadsRules(c *C) {
   167  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   168  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   169  		spec.AddSnippet("dummy")
   170  		return nil
   171  	}
   172  	for _, opts := range testedConfinementOpts {
   173  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   174  		s.udevadmCmd.ForgetCalls()
   175  		s.RemoveSnap(c, snapInfo)
   176  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   177  		// file called "70-snap.sambda.rules" was removed
   178  		_, err := os.Stat(fname)
   179  		c.Check(os.IsNotExist(err), Equals, true)
   180  		// udevadm was used to reload rules and re-run triggers
   181  		c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
   182  			{"udevadm", "control", "--reload-rules"},
   183  			{"udevadm", "trigger", "--subsystem-nomatch=input"},
   184  			// FIXME: temporary until spec.TriggerSubsystem() can
   185  			// be called during disconnect
   186  			{"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"},
   187  			{"udevadm", "settle", "--timeout=10"},
   188  		})
   189  	}
   190  }
   191  
   192  func (s *backendSuite) TestUpdatingSnapToOneWithMoreApps(c *C) {
   193  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   194  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   195  		spec.AddSnippet(createSnippetForApps(slot.Apps))
   196  		return nil
   197  	}
   198  	for _, opts := range testedConfinementOpts {
   199  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   200  		s.udevadmCmd.ForgetCalls()
   201  		snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1WithNmbd, 0)
   202  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   203  		// file called "70-snap.sambda.rules" was created
   204  		_, err := os.Stat(fname)
   205  		c.Check(err, IsNil)
   206  		// udevadm was used to reload rules and re-run triggers
   207  		c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
   208  			{"udevadm", "control", "--reload-rules"},
   209  			{"udevadm", "trigger", "--subsystem-nomatch=input"},
   210  			// FIXME: temporary until spec.TriggerSubsystem() can
   211  			// be called during disconnect
   212  			{"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"},
   213  			{"udevadm", "settle", "--timeout=10"},
   214  		})
   215  		s.RemoveSnap(c, snapInfo)
   216  	}
   217  }
   218  
   219  func (s *backendSuite) TestUpdatingSnapToOneWithMoreHooks(c *C) {
   220  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   221  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   222  		spec.AddSnippet(createSnippetForApps(slot.Apps))
   223  		return nil
   224  	}
   225  	s.Iface.UDevPermanentPlugCallback = func(spec *udev.Specification, slot *snap.PlugInfo) error {
   226  		spec.AddSnippet("dummy")
   227  		return nil
   228  	}
   229  	for _, opts := range testedConfinementOpts {
   230  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   231  		s.udevadmCmd.ForgetCalls()
   232  		snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlWithHook, 0)
   233  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   234  
   235  		// Verify that "70-snap.samba.rules" was created
   236  		_, err := os.Stat(fname)
   237  		c.Check(err, IsNil)
   238  
   239  		// Verify that udevadm was used to reload rules and re-run triggers
   240  		c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
   241  			{"udevadm", "control", "--reload-rules"},
   242  			{"udevadm", "trigger", "--subsystem-nomatch=input"},
   243  			// FIXME: temporary until spec.TriggerSubsystem() can
   244  			// be called during disconnect
   245  			{"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"},
   246  			{"udevadm", "settle", "--timeout=10"},
   247  		})
   248  		s.RemoveSnap(c, snapInfo)
   249  	}
   250  }
   251  
   252  func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) {
   253  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   254  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   255  		spec.AddSnippet(createSnippetForApps(slot.Apps))
   256  		return nil
   257  	}
   258  	for _, opts := range testedConfinementOpts {
   259  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1WithNmbd, 0)
   260  		s.udevadmCmd.ForgetCalls()
   261  		snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 0)
   262  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   263  		// file called "70-snap.sambda.rules" still exists
   264  		_, err := os.Stat(fname)
   265  		c.Check(err, IsNil)
   266  		// udevadm was used to reload rules and re-run triggers
   267  		c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
   268  			{"udevadm", "control", "--reload-rules"},
   269  			{"udevadm", "trigger", "--subsystem-nomatch=input"},
   270  			// FIXME: temporary until spec.TriggerSubsystem() can
   271  			// be called during disconnect
   272  			{"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"},
   273  			{"udevadm", "settle", "--timeout=10"},
   274  		})
   275  		s.RemoveSnap(c, snapInfo)
   276  	}
   277  }
   278  
   279  func (s *backendSuite) TestUpdatingSnapToOneWithFewerHooks(c *C) {
   280  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   281  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   282  		spec.AddSnippet(createSnippetForApps(slot.Apps))
   283  		return nil
   284  	}
   285  	s.Iface.UDevPermanentPlugCallback = func(spec *udev.Specification, slot *snap.PlugInfo) error {
   286  		spec.AddSnippet("dummy")
   287  		return nil
   288  	}
   289  	for _, opts := range testedConfinementOpts {
   290  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlWithHook, 0)
   291  		s.udevadmCmd.ForgetCalls()
   292  		snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 0)
   293  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   294  		// file called "70-snap.sambda.rules" still exists
   295  		_, err := os.Stat(fname)
   296  		c.Check(err, IsNil)
   297  		// Verify that udevadm was used to reload rules and re-run triggers
   298  		c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
   299  			{"udevadm", "control", "--reload-rules"},
   300  			{"udevadm", "trigger", "--subsystem-nomatch=input"},
   301  			// FIXME: temporary until spec.TriggerSubsystem() can
   302  			// be called during disconnect
   303  			{"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"},
   304  			{"udevadm", "settle", "--timeout=10"},
   305  		})
   306  		s.RemoveSnap(c, snapInfo)
   307  	}
   308  }
   309  
   310  func (s *backendSuite) TestCombineSnippetsWithActualSnippets(c *C) {
   311  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   312  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   313  		spec.AddSnippet("dummy")
   314  		return nil
   315  	}
   316  	for _, opts := range testedConfinementOpts {
   317  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   318  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   319  		if opts.DevMode || opts.Classic {
   320  			c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#dummy\n")
   321  		} else {
   322  			c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\ndummy\n")
   323  		}
   324  		stat, err := os.Stat(fname)
   325  		c.Assert(err, IsNil)
   326  		c.Check(stat.Mode(), Equals, os.FileMode(0644))
   327  		s.RemoveSnap(c, snapInfo)
   328  	}
   329  }
   330  
   331  func (s *backendSuite) TestControlsDeviceCgroup(c *C) {
   332  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   333  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   334  		spec.AddSnippet("dummy")
   335  		spec.SetControlsDeviceCgroup()
   336  		return nil
   337  	}
   338  	for _, opts := range testedConfinementOpts {
   339  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   340  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   341  		c.Check(fname, testutil.FileAbsent)
   342  		s.RemoveSnap(c, snapInfo)
   343  	}
   344  }
   345  
   346  func (s *backendSuite) TestCombineSnippetsWithActualSnippetsWithNewline(c *C) {
   347  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   348  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   349  		spec.AddSnippet("dummy1\ndummy2")
   350  		return nil
   351  	}
   352  	for _, opts := range testedConfinementOpts {
   353  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   354  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   355  		if opts.DevMode || opts.Classic {
   356  			c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#dummy1\n#dummy2\n")
   357  		} else {
   358  			c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\ndummy1\ndummy2\n")
   359  		}
   360  		stat, err := os.Stat(fname)
   361  		c.Assert(err, IsNil)
   362  		c.Check(stat.Mode(), Equals, os.FileMode(0644))
   363  		s.RemoveSnap(c, snapInfo)
   364  	}
   365  }
   366  func (s *backendSuite) TestCombineSnippetsWithActualSnippetsWhenPlugNoApps(c *C) {
   367  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   368  	s.Iface.UDevPermanentPlugCallback = func(spec *udev.Specification, slot *snap.PlugInfo) error {
   369  		spec.AddSnippet("dummy")
   370  		return nil
   371  	}
   372  	for _, opts := range testedConfinementOpts {
   373  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.PlugNoAppsYaml, 0)
   374  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.foo.rules")
   375  		if opts.DevMode || opts.Classic {
   376  			c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#dummy\n")
   377  		} else {
   378  			c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\ndummy\n")
   379  		}
   380  		stat, err := os.Stat(fname)
   381  		c.Assert(err, IsNil)
   382  		c.Check(stat.Mode(), Equals, os.FileMode(0644))
   383  		s.RemoveSnap(c, snapInfo)
   384  	}
   385  }
   386  
   387  func (s *backendSuite) TestCombineSnippetsWithActualSnippetsWhenSlotNoApps(c *C) {
   388  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   389  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   390  		spec.AddSnippet("dummy")
   391  		return nil
   392  	}
   393  	for _, opts := range testedConfinementOpts {
   394  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SlotNoAppsYaml, 0)
   395  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.foo.rules")
   396  		if opts.DevMode || opts.Classic {
   397  			c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#dummy\n")
   398  		} else {
   399  			c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\ndummy\n")
   400  		}
   401  		stat, err := os.Stat(fname)
   402  		c.Assert(err, IsNil)
   403  		c.Check(stat.Mode(), Equals, os.FileMode(0644))
   404  		s.RemoveSnap(c, snapInfo)
   405  	}
   406  }
   407  
   408  func (s *backendSuite) TestCombineSnippetsWithoutAnySnippets(c *C) {
   409  	for _, opts := range testedConfinementOpts {
   410  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   411  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   412  		_, err := os.Stat(fname)
   413  		// Without any snippets, there the .rules file is not created.
   414  		c.Check(os.IsNotExist(err), Equals, true)
   415  		s.RemoveSnap(c, snapInfo)
   416  	}
   417  }
   418  
   419  func (s *backendSuite) TestUpdatingSnapToOneWithoutSlots(c *C) {
   420  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   421  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   422  		spec.AddSnippet("dummy")
   423  		return nil
   424  	}
   425  	for _, opts := range testedConfinementOpts {
   426  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   427  		s.udevadmCmd.ForgetCalls()
   428  		snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1NoSlot, 0)
   429  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   430  		// file called "70-snap.sambda.rules" was removed
   431  		_, err := os.Stat(fname)
   432  		c.Check(os.IsNotExist(err), Equals, true)
   433  		// Verify that udevadm was used to reload rules and re-run triggers
   434  		c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
   435  			{"udevadm", "control", "--reload-rules"},
   436  			{"udevadm", "trigger", "--subsystem-nomatch=input"},
   437  			// FIXME: temporary until spec.TriggerSubsystem() can
   438  			// be called during disconnect
   439  			{"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"},
   440  			{"udevadm", "settle", "--timeout=10"},
   441  		})
   442  		s.RemoveSnap(c, snapInfo)
   443  	}
   444  }
   445  
   446  func (s *backendSuite) TestUpdatingSnapWithoutSlotsToOneWithoutSlots(c *C) {
   447  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   448  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   449  		spec.AddSnippet("dummy")
   450  		return nil
   451  	}
   452  	for _, opts := range testedConfinementOpts {
   453  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1NoSlot, 0)
   454  		// file called "70-snap.sambda.rules" does not exist
   455  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   456  		_, err := os.Stat(fname)
   457  		c.Check(os.IsNotExist(err), Equals, true)
   458  		s.udevadmCmd.ForgetCalls()
   459  
   460  		snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1WithNmbdNoSlot, 0)
   461  		// file called "70-snap.sambda.rules" still does not exist
   462  		_, err = os.Stat(fname)
   463  		c.Check(os.IsNotExist(err), Equals, true)
   464  		// Verify that udevadm was used to reload rules and re-run triggers
   465  		c.Check(len(s.udevadmCmd.Calls()), Equals, 0)
   466  		s.RemoveSnap(c, snapInfo)
   467  	}
   468  }
   469  
   470  func (s *backendSuite) TestInstallingSnapWritesAndLoadsRulesWithInputSubsystem(c *C) {
   471  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   472  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   473  		spec.TriggerSubsystem("input")
   474  		spec.AddSnippet("dummy")
   475  		return nil
   476  	}
   477  	for _, opts := range testedConfinementOpts {
   478  		s.udevadmCmd.ForgetCalls()
   479  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   480  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   481  		// file called "70-snap.sambda.rules" was created
   482  		_, err := os.Stat(fname)
   483  		c.Check(err, IsNil)
   484  		// udevadm was used to reload rules and re-run triggers
   485  		c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
   486  			{"udevadm", "control", "--reload-rules"},
   487  			{"udevadm", "trigger", "--subsystem-nomatch=input"},
   488  			{"udevadm", "trigger", "--subsystem-match=input"},
   489  			{"udevadm", "settle", "--timeout=10"},
   490  		})
   491  		s.RemoveSnap(c, snapInfo)
   492  	}
   493  }
   494  
   495  func (s *backendSuite) TestInstallingSnapWritesAndLoadsRulesWithInputJoystickSubsystem(c *C) {
   496  	// NOTE: Hand out a permanent snippet so that .rules file is generated.
   497  	s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error {
   498  		spec.TriggerSubsystem("input/joystick")
   499  		spec.AddSnippet("dummy")
   500  		return nil
   501  	}
   502  	for _, opts := range testedConfinementOpts {
   503  		s.udevadmCmd.ForgetCalls()
   504  		snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0)
   505  		fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules")
   506  		// file called "70-snap.sambda.rules" was created
   507  		_, err := os.Stat(fname)
   508  		c.Check(err, IsNil)
   509  		// udevadm was used to reload rules and re-run triggers
   510  		c.Check(s.udevadmCmd.Calls(), DeepEquals, [][]string{
   511  			{"udevadm", "control", "--reload-rules"},
   512  			{"udevadm", "trigger", "--subsystem-nomatch=input"},
   513  			{"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"},
   514  			{"udevadm", "settle", "--timeout=10"},
   515  		})
   516  		s.RemoveSnap(c, snapInfo)
   517  	}
   518  }
   519  
   520  func (s *backendSuite) TestSandboxFeatures(c *C) {
   521  	restore := cgroup.MockVersion(cgroup.V1, nil)
   522  	defer restore()
   523  
   524  	c.Assert(s.Backend.SandboxFeatures(), DeepEquals, []string{
   525  		"device-filtering",
   526  		"device-cgroup-v1",
   527  		"tagging",
   528  	})
   529  
   530  	restore = cgroup.MockVersion(cgroup.V2, nil)
   531  	defer restore()
   532  	c.Assert(s.Backend.SandboxFeatures(), DeepEquals, []string{
   533  		"tagging",
   534  	})
   535  }