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