github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/ifacestate/hotplug_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 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 ifacestate_test
    21  
    22  import (
    23  	"crypto/sha256"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  	"time"
    28  
    29  	. "gopkg.in/check.v1"
    30  
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/interfaces"
    33  	"github.com/snapcore/snapd/interfaces/builtin"
    34  	"github.com/snapcore/snapd/interfaces/hotplug"
    35  	"github.com/snapcore/snapd/interfaces/ifacetest"
    36  	"github.com/snapcore/snapd/osutil"
    37  	"github.com/snapcore/snapd/overlord"
    38  	"github.com/snapcore/snapd/overlord/configstate/config"
    39  	"github.com/snapcore/snapd/overlord/hookstate"
    40  	"github.com/snapcore/snapd/overlord/ifacestate"
    41  	"github.com/snapcore/snapd/overlord/ifacestate/udevmonitor"
    42  	"github.com/snapcore/snapd/overlord/snapstate"
    43  	"github.com/snapcore/snapd/overlord/state"
    44  	"github.com/snapcore/snapd/snap"
    45  	"github.com/snapcore/snapd/snap/snaptest"
    46  	"github.com/snapcore/snapd/testutil"
    47  )
    48  
    49  type hotplugSuite struct {
    50  	testutil.BaseTest
    51  	AssertsMock
    52  
    53  	o           *overlord.Overlord
    54  	state       *state.State
    55  	secBackend  *ifacetest.TestSecurityBackend
    56  	mockSnapCmd *testutil.MockCmd
    57  
    58  	udevMon               *udevMonitorMock
    59  	mgr                   *ifacestate.InterfaceManager
    60  	handledByGadgetCalled int
    61  
    62  	ifaceTestAAutoConnect bool
    63  }
    64  
    65  type hotplugTasksWitness struct {
    66  	seenHooks              map[string]string
    67  	seenHotplugRemoveKeys  map[snap.HotplugKey]string
    68  	seenHotplugConnectKeys map[snap.HotplugKey]string
    69  	seenHotplugUpdateKeys  map[snap.HotplugKey]string
    70  	seenHotplugAddKeys     map[snap.HotplugKey]string
    71  	seenTasks              map[string]int
    72  	hotplugDisconnects     map[snap.HotplugKey]string
    73  	connects               []string
    74  	disconnects            []string
    75  }
    76  
    77  func (w *hotplugTasksWitness) checkTasks(c *C, st *state.State) {
    78  	w.seenTasks = make(map[string]int)
    79  	w.seenHotplugRemoveKeys = make(map[snap.HotplugKey]string)
    80  	w.seenHotplugConnectKeys = make(map[snap.HotplugKey]string)
    81  	w.seenHotplugUpdateKeys = make(map[snap.HotplugKey]string)
    82  	w.seenHotplugAddKeys = make(map[snap.HotplugKey]string)
    83  	w.hotplugDisconnects = make(map[snap.HotplugKey]string)
    84  	w.seenHooks = make(map[string]string)
    85  	for _, t := range st.Tasks() {
    86  		c.Check(t.Status(), Equals, state.DoneStatus)
    87  		if t.Kind() == "run-hook" {
    88  			var hookSup hookstate.HookSetup
    89  			c.Assert(t.Get("hook-setup", &hookSup), IsNil)
    90  			_, ok := w.seenHooks[hookSup.Hook]
    91  			c.Assert(ok, Equals, false)
    92  			w.seenHooks[hookSup.Hook] = hookSup.Snap
    93  			continue
    94  		}
    95  		w.seenTasks[t.Kind()]++
    96  		if t.Kind() == "connect" || t.Kind() == "disconnect" {
    97  			var plugRef interfaces.PlugRef
    98  			var slotRef interfaces.SlotRef
    99  			c.Assert(t.Get("plug", &plugRef), IsNil)
   100  			c.Assert(t.Get("slot", &slotRef), IsNil)
   101  			if t.Kind() == "connect" {
   102  				w.connects = append(w.connects, fmt.Sprintf("%s %s", plugRef, slotRef))
   103  			} else {
   104  				testByHotplugTaskFlag(c, t)
   105  				w.disconnects = append(w.disconnects, fmt.Sprintf("%s %s", plugRef, slotRef))
   106  			}
   107  			continue
   108  		}
   109  
   110  		if t.Kind() == "hotplug-seq-wait" {
   111  			continue
   112  		}
   113  
   114  		iface, key, err := ifacestate.GetHotplugAttrs(t)
   115  		c.Check(err, IsNil)
   116  
   117  		switch {
   118  		case t.Kind() == "hotplug-add-slot":
   119  			w.seenHotplugAddKeys[key] = iface
   120  		case t.Kind() == "hotplug-connect":
   121  			w.seenHotplugConnectKeys[key] = iface
   122  		case t.Kind() == "hotplug-update-slot":
   123  			w.seenHotplugUpdateKeys[key] = iface
   124  		case t.Kind() == "hotplug-remove-slot":
   125  			w.seenHotplugRemoveKeys[key] = iface
   126  		case t.Kind() == "hotplug-disconnect":
   127  			w.hotplugDisconnects[key] = iface
   128  		}
   129  	}
   130  }
   131  
   132  var _ = Suite(&hotplugSuite{})
   133  
   134  func (s *hotplugSuite) SetUpTest(c *C) {
   135  	s.BaseTest.SetUpTest(c)
   136  	s.secBackend = &ifacetest.TestSecurityBackend{}
   137  	s.BaseTest.AddCleanup(ifacestate.MockSecurityBackends([]interfaces.SecurityBackend{s.secBackend}))
   138  
   139  	dirs.SetRootDir(c.MkDir())
   140  	c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapSystemKeyFile), 0755), IsNil)
   141  
   142  	restore := osutil.MockMountInfo("")
   143  	s.AddCleanup(restore)
   144  
   145  	s.o = overlord.Mock()
   146  	s.state = s.o.State()
   147  
   148  	s.mockSnapCmd = testutil.MockCommand(c, "snap", "")
   149  
   150  	s.SetupAsserts(c, s.state, &s.BaseTest)
   151  
   152  	restoreTimeout := ifacestate.MockUDevInitRetryTimeout(0 * time.Second)
   153  	s.BaseTest.AddCleanup(restoreTimeout)
   154  
   155  	s.udevMon = &udevMonitorMock{}
   156  	restoreCreate := ifacestate.MockCreateUDevMonitor(func(add udevmonitor.DeviceAddedFunc, remove udevmonitor.DeviceRemovedFunc, done udevmonitor.EnumerationDoneFunc) udevmonitor.Interface {
   157  		s.udevMon.AddDevice = add
   158  		s.udevMon.RemoveDevice = remove
   159  		s.udevMon.EnumerationDone = done
   160  		return s.udevMon
   161  	})
   162  	s.BaseTest.AddCleanup(restoreCreate)
   163  
   164  	// mock core snap
   165  	si := &snap.SideInfo{RealName: "core", Revision: snap.R(1)}
   166  	snaptest.MockSnapInstance(c, "", coreSnapYaml, si)
   167  	s.state.Lock()
   168  	snapstate.Set(s.state, "core", &snapstate.SnapState{
   169  		Active:   true,
   170  		Sequence: []*snap.SideInfo{si},
   171  		Current:  snap.R(1),
   172  		SnapType: "os",
   173  	})
   174  
   175  	tr := config.NewTransaction(s.state)
   176  	tr.Set("core", "experimental.hotplug", true)
   177  	tr.Commit()
   178  
   179  	s.state.Unlock()
   180  
   181  	hookMgr, err := hookstate.Manager(s.state, s.o.TaskRunner())
   182  	c.Assert(err, IsNil)
   183  	s.o.AddManager(hookMgr)
   184  
   185  	s.mgr, err = ifacestate.Manager(s.state, hookMgr, s.o.TaskRunner(), nil, nil)
   186  	c.Assert(err, IsNil)
   187  
   188  	s.o.AddManager(s.mgr)
   189  	s.o.AddManager(s.o.TaskRunner())
   190  
   191  	// startup
   192  	err = s.o.StartUp()
   193  	c.Assert(err, IsNil)
   194  
   195  	autoConnectNo := func(*snap.PlugInfo, *snap.SlotInfo) bool {
   196  		return false
   197  	}
   198  	s.ifaceTestAAutoConnect = false
   199  	testAAutoConnect := func(*snap.PlugInfo, *snap.SlotInfo) bool {
   200  		return s.ifaceTestAAutoConnect
   201  	}
   202  
   203  	testIface1 := &ifacetest.TestHotplugInterface{
   204  		TestInterface: ifacetest.TestInterface{
   205  			InterfaceName:       "test-a",
   206  			AutoConnectCallback: testAAutoConnect,
   207  		},
   208  		HotplugKeyCallback: func(deviceInfo *hotplug.HotplugDeviceInfo) (snap.HotplugKey, error) {
   209  			return "key-1", nil
   210  		},
   211  		HotplugDeviceDetectedCallback: func(deviceInfo *hotplug.HotplugDeviceInfo) (*hotplug.ProposedSlot, error) {
   212  			return &hotplug.ProposedSlot{
   213  				Name: "hotplugslot-a",
   214  				Attrs: map[string]interface{}{
   215  					"slot-a-attr1": "a",
   216  					"path":         deviceInfo.DevicePath(),
   217  				}}, nil
   218  		},
   219  	}
   220  	testIface2 := &ifacetest.TestHotplugInterface{
   221  		TestInterface: ifacetest.TestInterface{
   222  			InterfaceName:       "test-b",
   223  			AutoConnectCallback: autoConnectNo,
   224  		},
   225  		HotplugKeyCallback: func(deviceInfo *hotplug.HotplugDeviceInfo) (snap.HotplugKey, error) {
   226  			return "key-2", nil
   227  		},
   228  		HotplugDeviceDetectedCallback: func(deviceInfo *hotplug.HotplugDeviceInfo) (*hotplug.ProposedSlot, error) {
   229  			return &hotplug.ProposedSlot{Name: "hotplugslot-b"}, nil
   230  		},
   231  		HandledByGadgetCallback: func(di *hotplug.HotplugDeviceInfo, slot *snap.SlotInfo) bool {
   232  			s.handledByGadgetCalled++
   233  			var path string
   234  			slot.Attr("path", &path)
   235  			return di.DeviceName() == path
   236  		},
   237  	}
   238  	// 3rd hotplug interface doesn't create hotplug slot (to simulate a case where doesn't device is not supported)
   239  	testIface3 := &ifacetest.TestHotplugInterface{
   240  		TestInterface: ifacetest.TestInterface{
   241  			InterfaceName:       "test-c",
   242  			AutoConnectCallback: autoConnectNo,
   243  		},
   244  		HotplugKeyCallback: func(deviceInfo *hotplug.HotplugDeviceInfo) (snap.HotplugKey, error) {
   245  			return "key-3", nil
   246  		},
   247  		HotplugDeviceDetectedCallback: func(deviceInfo *hotplug.HotplugDeviceInfo) (*hotplug.ProposedSlot, error) {
   248  			return nil, nil
   249  		},
   250  	}
   251  	// 3rd hotplug interface will only create a slot if default hotplug key can be computed
   252  	testIface4 := &ifacetest.TestHotplugInterface{
   253  		TestInterface: ifacetest.TestInterface{
   254  			InterfaceName:       "test-d",
   255  			AutoConnectCallback: autoConnectNo,
   256  		},
   257  		HotplugDeviceDetectedCallback: func(deviceInfo *hotplug.HotplugDeviceInfo) (*hotplug.ProposedSlot, error) {
   258  			return &hotplug.ProposedSlot{Name: "hotplugslot-d"}, nil
   259  		},
   260  	}
   261  
   262  	for _, iface := range []interfaces.Interface{testIface1, testIface2, testIface3, testIface4} {
   263  		c.Assert(s.mgr.Repository().AddInterface(iface), IsNil)
   264  		s.AddCleanup(builtin.MockInterface(iface))
   265  	}
   266  
   267  	// single Ensure to have udev monitor created and wired up by interface manager
   268  	c.Assert(s.mgr.Ensure(), IsNil)
   269  }
   270  
   271  func (s *hotplugSuite) TearDownTest(c *C) {
   272  	s.BaseTest.TearDownTest(c)
   273  	dirs.SetRootDir("")
   274  	s.mockSnapCmd.Restore()
   275  }
   276  
   277  func testHotplugTaskAttrs(c *C, t *state.Task, ifaceName, hotplugKey string) {
   278  	iface, key, err := ifacestate.GetHotplugAttrs(t)
   279  	c.Assert(err, IsNil)
   280  	c.Assert(key, Equals, snap.HotplugKey(hotplugKey))
   281  	c.Assert(iface, Equals, ifaceName)
   282  }
   283  
   284  func testByHotplugTaskFlag(c *C, t *state.Task) {
   285  	var byHotplug bool
   286  	c.Assert(t.Get("by-hotplug", &byHotplug), IsNil)
   287  	c.Assert(byHotplug, Equals, true)
   288  }
   289  
   290  func (s *hotplugSuite) TestHotplugAddBasic(c *C) {
   291  	s.MockModel(c, nil)
   292  
   293  	di, err := hotplug.NewHotplugDeviceInfo(map[string]string{"DEVPATH": "a/path", "ACTION": "add", "SUBSYSTEM": "foo"})
   294  	c.Assert(err, IsNil)
   295  	s.udevMon.AddDevice(di)
   296  
   297  	c.Assert(s.o.Settle(5*time.Second), IsNil)
   298  
   299  	st := s.state
   300  	st.Lock()
   301  	defer st.Unlock()
   302  
   303  	var hp hotplugTasksWitness
   304  	hp.checkTasks(c, st)
   305  	c.Check(hp.seenTasks, DeepEquals, map[string]int{"hotplug-seq-wait": 2, "hotplug-add-slot": 2, "hotplug-connect": 2})
   306  	c.Check(hp.seenHotplugAddKeys, DeepEquals, map[snap.HotplugKey]string{"key-1": "test-a", "key-2": "test-b"})
   307  	c.Check(hp.seenHotplugConnectKeys, DeepEquals, map[snap.HotplugKey]string{"key-1": "test-a", "key-2": "test-b"})
   308  	c.Check(hp.seenHooks, HasLen, 0)
   309  	c.Check(hp.connects, HasLen, 0)
   310  
   311  	// make sure slots have been created in the repo
   312  	repo := s.mgr.Repository()
   313  	slot, err := repo.SlotForHotplugKey("test-a", "key-1")
   314  	c.Assert(err, IsNil)
   315  	c.Assert(slot, NotNil)
   316  	slots := repo.AllSlots("test-a")
   317  	c.Assert(slots, HasLen, 1)
   318  	c.Check(slots[0].Name, Equals, "hotplugslot-a")
   319  	c.Check(slots[0].Attrs, DeepEquals, map[string]interface{}{
   320  		"path":         di.DevicePath(),
   321  		"slot-a-attr1": "a"})
   322  	c.Check(slots[0].HotplugKey, DeepEquals, snap.HotplugKey("key-1"))
   323  
   324  	slot, err = repo.SlotForHotplugKey("test-b", "key-2")
   325  	c.Assert(err, IsNil)
   326  	c.Assert(slot, NotNil)
   327  
   328  	slot, err = repo.SlotForHotplugKey("test-c", "key-3")
   329  	c.Assert(err, IsNil)
   330  	c.Assert(slot, IsNil)
   331  
   332  	c.Check(s.handledByGadgetCalled, Equals, 0)
   333  }
   334  
   335  func (s *hotplugSuite) TestHotplugConnectWithGadgetSlot(c *C) {
   336  	s.MockModel(c, map[string]interface{}{
   337  		"gadget": "the-gadget",
   338  	})
   339  
   340  	st := s.state
   341  	st.Lock()
   342  	defer st.Unlock()
   343  
   344  	gadgetSideInfo := &snap.SideInfo{RealName: "the-gadget", SnapID: "the-gadget-id", Revision: snap.R(1)}
   345  	gadgetInfo := snaptest.MockSnap(c, `
   346  name: the-gadget
   347  type: gadget
   348  version: 1.0
   349  
   350  slots:
   351    slot1:
   352      interface: test-b
   353      path: /dev/path
   354  `, gadgetSideInfo)
   355  	snapstate.Set(s.state, "the-gadget", &snapstate.SnapState{
   356  		Active:   true,
   357  		Sequence: []*snap.SideInfo{&gadgetInfo.SideInfo},
   358  		Current:  snap.R(1),
   359  		SnapType: "gadget"})
   360  	st.Unlock()
   361  
   362  	di, err := hotplug.NewHotplugDeviceInfo(map[string]string{
   363  		"DEVNAME":   "/dev/path",
   364  		"DEVPATH":   "a/path",
   365  		"ACTION":    "add",
   366  		"SUBSYSTEM": "foo"})
   367  	c.Assert(err, IsNil)
   368  	s.udevMon.AddDevice(di)
   369  
   370  	c.Assert(s.o.Settle(5*time.Second), IsNil)
   371  	st.Lock()
   372  
   373  	c.Check(s.handledByGadgetCalled, Equals, 1)
   374  
   375  	// make sure hotplug slot has been created in the repo
   376  	repo := s.mgr.Repository()
   377  	slot, err := repo.SlotForHotplugKey("test-a", "key-1")
   378  	c.Assert(err, IsNil)
   379  	c.Assert(slot, NotNil)
   380  
   381  	// but no hotplug slot has been created for the device path defined by gadget
   382  	slot, err = repo.SlotForHotplugKey("test-b", "key-2")
   383  	c.Assert(err, IsNil)
   384  	c.Assert(slot, IsNil)
   385  }
   386  
   387  func (s *hotplugSuite) TestHotplugAddWithDefaultKey(c *C) {
   388  	s.MockModel(c, nil)
   389  
   390  	di, err := hotplug.NewHotplugDeviceInfo(map[string]string{
   391  		"DEVPATH":         "a/path",
   392  		"ACTION":          "add",
   393  		"SUBSYSTEM":       "foo",
   394  		"ID_VENDOR_ID":    "vendor",
   395  		"ID_MODEL_ID":     "model",
   396  		"ID_SERIAL_SHORT": "serial",
   397  	})
   398  	c.Assert(err, IsNil)
   399  	s.udevMon.AddDevice(di)
   400  
   401  	c.Assert(s.o.Settle(5*time.Second), IsNil)
   402  
   403  	st := s.state
   404  	st.Lock()
   405  	defer st.Unlock()
   406  
   407  	var hp hotplugTasksWitness
   408  	hp.checkTasks(c, st)
   409  	c.Check(hp.seenTasks, DeepEquals, map[string]int{"hotplug-seq-wait": 3, "hotplug-add-slot": 3, "hotplug-connect": 3})
   410  	c.Check(hp.seenHooks, HasLen, 0)
   411  	c.Check(hp.connects, HasLen, 0)
   412  	testIfaceDkey := keyHelper("ID_VENDOR_ID\x00vendor\x00ID_MODEL_ID\x00model\x00ID_SERIAL_SHORT\x00serial\x00")
   413  	c.Assert(hp.seenHotplugAddKeys, DeepEquals, map[snap.HotplugKey]string{
   414  		"key-1":       "test-a",
   415  		"key-2":       "test-b",
   416  		testIfaceDkey: "test-d"})
   417  	c.Assert(hp.seenHotplugConnectKeys, DeepEquals, map[snap.HotplugKey]string{
   418  		"key-1":       "test-a",
   419  		"key-2":       "test-b",
   420  		testIfaceDkey: "test-d"})
   421  
   422  	// make sure the slot has been created
   423  	repo := s.mgr.Repository()
   424  	slots := repo.AllSlots("test-d")
   425  	c.Assert(slots, HasLen, 1)
   426  	c.Check(slots[0].Name, Equals, "hotplugslot-d")
   427  	c.Check(slots[0].HotplugKey, Equals, testIfaceDkey)
   428  }
   429  
   430  func (s *hotplugSuite) TestHotplugAddWithAutoconnect(c *C) {
   431  	s.MockModel(c, nil)
   432  
   433  	s.ifaceTestAAutoConnect = true
   434  
   435  	repo := s.mgr.Repository()
   436  	st := s.state
   437  
   438  	st.Lock()
   439  	// mock the consumer snap/plug
   440  	si := &snap.SideInfo{RealName: "consumer", Revision: snap.R(1)}
   441  	testSnap := snaptest.MockSnapInstance(c, "", testSnapYaml, si)
   442  	c.Assert(testSnap.Plugs, HasLen, 1)
   443  	c.Assert(testSnap.Plugs["plug"], NotNil)
   444  	c.Assert(repo.AddPlug(testSnap.Plugs["plug"]), IsNil)
   445  	snapstate.Set(s.state, "consumer", &snapstate.SnapState{
   446  		Active:   true,
   447  		Sequence: []*snap.SideInfo{si},
   448  		Current:  snap.R(1),
   449  		SnapType: "app",
   450  	})
   451  	st.Unlock()
   452  
   453  	di, err := hotplug.NewHotplugDeviceInfo(map[string]string{"DEVPATH": "a/path", "ACTION": "add", "SUBSYSTEM": "foo"})
   454  	c.Assert(err, IsNil)
   455  	s.udevMon.AddDevice(di)
   456  
   457  	c.Assert(s.o.Settle(5*time.Second), IsNil)
   458  	st.Lock()
   459  	defer st.Unlock()
   460  
   461  	// verify hotplug tasks
   462  	var hp hotplugTasksWitness
   463  	hp.checkTasks(c, st)
   464  	c.Check(hp.seenTasks, DeepEquals, map[string]int{"hotplug-seq-wait": 2, "hotplug-add-slot": 2, "hotplug-connect": 2, "connect": 1})
   465  	c.Check(hp.seenHooks, DeepEquals, map[string]string{"prepare-plug-plug": "consumer", "connect-plug-plug": "consumer"})
   466  	c.Check(hp.seenHotplugAddKeys, DeepEquals, map[snap.HotplugKey]string{"key-1": "test-a", "key-2": "test-b"})
   467  	c.Check(hp.seenHotplugConnectKeys, DeepEquals, map[snap.HotplugKey]string{"key-1": "test-a", "key-2": "test-b"})
   468  	c.Check(hp.connects, DeepEquals, []string{"consumer:plug core:hotplugslot-a"})
   469  
   470  	// make sure slots have been created in the repo
   471  	slot, err := repo.SlotForHotplugKey("test-a", "key-1")
   472  	c.Assert(err, IsNil)
   473  	c.Assert(slot, NotNil)
   474  
   475  	conn, err := repo.Connection(&interfaces.ConnRef{
   476  		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
   477  		SlotRef: interfaces.SlotRef{Snap: "core", Name: "hotplugslot-a"}})
   478  	c.Assert(err, IsNil)
   479  	c.Assert(conn, NotNil)
   480  }
   481  
   482  var testSnapYaml = `
   483  name: consumer
   484  version: 1
   485  plugs:
   486   plug:
   487    interface: test-a
   488  hooks:
   489   prepare-plug-plug:
   490   connect-plug-plug:
   491   disconnect-plug-plug:
   492  `
   493  
   494  func (s *hotplugSuite) TestHotplugRemove(c *C) {
   495  	st := s.state
   496  	st.Lock()
   497  
   498  	st.Set("conns", map[string]interface{}{
   499  		"consumer:plug core:hotplugslot": map[string]interface{}{
   500  			"interface":    "test-a",
   501  			"hotplug-key":  "key-1",
   502  			"hotplug-gone": false}})
   503  	st.Set("hotplug-slots", map[string]interface{}{
   504  		"hotplugslot": map[string]interface{}{
   505  			"name":         "hotplugslot",
   506  			"interface":    "test-a",
   507  			"hotplug-key":  "key-1",
   508  			"hotplug-gone": false}})
   509  
   510  	repo := s.mgr.Repository()
   511  	si := &snap.SideInfo{RealName: "consumer", Revision: snap.R(1)}
   512  	testSnap := snaptest.MockSnapInstance(c, "", testSnapYaml, si)
   513  	c.Assert(repo.AddPlug(&snap.PlugInfo{
   514  		Interface: "test-a",
   515  		Name:      "plug",
   516  		Attrs:     map[string]interface{}{},
   517  		Snap:      testSnap,
   518  	}), IsNil)
   519  	snapstate.Set(s.state, "consumer", &snapstate.SnapState{
   520  		Active:   true,
   521  		Sequence: []*snap.SideInfo{si},
   522  		Current:  snap.R(1),
   523  		SnapType: "app",
   524  	})
   525  
   526  	core, err := snapstate.CurrentInfo(s.state, "core")
   527  	c.Assert(err, IsNil)
   528  	c.Assert(repo.AddSlot(&snap.SlotInfo{
   529  		Interface:  "test-a",
   530  		Name:       "hotplugslot",
   531  		Attrs:      map[string]interface{}{},
   532  		Snap:       core,
   533  		HotplugKey: "key-1",
   534  	}), IsNil)
   535  
   536  	conn, err := repo.Connect(&interfaces.ConnRef{
   537  		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
   538  		SlotRef: interfaces.SlotRef{Snap: "core", Name: "hotplugslot"},
   539  	}, nil, nil, nil, nil, nil)
   540  	c.Assert(err, IsNil)
   541  	c.Assert(conn, NotNil)
   542  
   543  	restore := s.mgr.MockObservedDevicePath(filepath.Join(dirs.SysfsDir, "a/path"), "test-a", "key-1")
   544  	defer restore()
   545  
   546  	st.Unlock()
   547  
   548  	slot, _ := repo.SlotForHotplugKey("test-a", "key-1")
   549  	c.Assert(slot, NotNil)
   550  
   551  	di, err := hotplug.NewHotplugDeviceInfo(map[string]string{"DEVPATH": "a/path", "ACTION": "remove", "SUBSYSTEM": "foo"})
   552  	c.Assert(err, IsNil)
   553  	s.udevMon.RemoveDevice(di)
   554  
   555  	c.Assert(s.o.Settle(5*time.Second), IsNil)
   556  
   557  	st.Lock()
   558  	defer st.Unlock()
   559  
   560  	// verify hotplug tasks
   561  	var hp hotplugTasksWitness
   562  	hp.checkTasks(c, st)
   563  	c.Check(hp.seenHooks, DeepEquals, map[string]string{"disconnect-plug-plug": "consumer"})
   564  	c.Check(hp.seenHotplugRemoveKeys, DeepEquals, map[snap.HotplugKey]string{"key-1": "test-a"})
   565  	c.Check(hp.hotplugDisconnects, DeepEquals, map[snap.HotplugKey]string{"key-1": "test-a"})
   566  	c.Check(hp.seenTasks, DeepEquals, map[string]int{"hotplug-seq-wait": 1, "hotplug-disconnect": 1, "disconnect": 1, "hotplug-remove-slot": 1})
   567  	c.Check(hp.disconnects, DeepEquals, []string{"consumer:plug core:hotplugslot"})
   568  
   569  	slot, _ = repo.SlotForHotplugKey("test-a", "key-1")
   570  	c.Assert(slot, IsNil)
   571  
   572  	var newconns map[string]interface{}
   573  	c.Assert(st.Get("conns", &newconns), IsNil)
   574  	c.Assert(newconns, DeepEquals, map[string]interface{}{
   575  		"consumer:plug core:hotplugslot": map[string]interface{}{
   576  			"interface":    "test-a",
   577  			"hotplug-key":  "key-1",
   578  			"hotplug-gone": true}})
   579  }
   580  
   581  func (s *hotplugSuite) TestHotplugEnumerationDone(c *C) {
   582  	s.MockModel(c, nil)
   583  
   584  	st := s.state
   585  	st.Lock()
   586  
   587  	// existing connection
   588  	st.Set("conns", map[string]interface{}{
   589  		"consumer:plug core:hotplugslot": map[string]interface{}{
   590  			"interface":    "test-a",
   591  			"hotplug-key":  "key-other-device",
   592  			"hotplug-gone": false}})
   593  
   594  	repo := s.mgr.Repository()
   595  
   596  	si := &snap.SideInfo{RealName: "consumer", Revision: snap.R(1)}
   597  	testSnap := snaptest.MockSnapInstance(c, "", testSnapYaml, si)
   598  	c.Assert(repo.AddPlug(&snap.PlugInfo{
   599  		Interface: "test-a",
   600  		Name:      "plug",
   601  		Attrs:     map[string]interface{}{},
   602  		Snap:      testSnap,
   603  	}), IsNil)
   604  	snapstate.Set(s.state, "consumer", &snapstate.SnapState{
   605  		Active:   true,
   606  		Sequence: []*snap.SideInfo{si},
   607  		Current:  snap.R(1),
   608  		SnapType: "app"})
   609  
   610  	core, err := snapstate.CurrentInfo(s.state, "core")
   611  	c.Assert(err, IsNil)
   612  	c.Assert(repo.AddSlot(&snap.SlotInfo{
   613  		Interface:  "test-a",
   614  		Name:       "hotplugslot",
   615  		Attrs:      map[string]interface{}{},
   616  		Snap:       core,
   617  		HotplugKey: "key-other-device",
   618  	}), IsNil)
   619  
   620  	conn, err := repo.Connect(&interfaces.ConnRef{
   621  		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
   622  		SlotRef: interfaces.SlotRef{Snap: "core", Name: "hotplugslot"},
   623  	}, nil, nil, nil, nil, nil)
   624  	c.Assert(err, IsNil)
   625  	c.Assert(conn, NotNil)
   626  
   627  	st.Set("hotplug-slots", map[string]interface{}{
   628  		"hotplugslot": map[string]interface{}{
   629  			"name":        "hotplugslot",
   630  			"interface":   "test-a",
   631  			"hotplug-key": "key-other-device"},
   632  		"anotherslot": map[string]interface{}{
   633  			"name":        "anotherslot",
   634  			"interface":   "test-a",
   635  			"hotplug-key": "yet-another-device"}})
   636  
   637  	// sanity
   638  	slot, _ := repo.SlotForHotplugKey("test-a", "key-other-device")
   639  	c.Assert(slot, NotNil)
   640  
   641  	st.Unlock()
   642  
   643  	// new device added; device for existing connection not present when enumeration is finished
   644  	di, err := hotplug.NewHotplugDeviceInfo(map[string]string{"DEVPATH": "a/path", "ACTION": "add", "SUBSYSTEM": "foo"})
   645  	c.Assert(err, IsNil)
   646  	s.udevMon.AddDevice(di)
   647  	s.udevMon.EnumerationDone()
   648  
   649  	c.Assert(s.o.Settle(5*time.Second), IsNil)
   650  
   651  	s.state.Lock()
   652  	defer s.state.Unlock()
   653  
   654  	// make sure slots for new device have been created in the repo
   655  	hpslot, _ := repo.SlotForHotplugKey("test-a", "key-1")
   656  	c.Assert(hpslot, NotNil)
   657  	hpslot, _ = repo.SlotForHotplugKey("test-b", "key-2")
   658  	c.Assert(hpslot, NotNil)
   659  
   660  	// make sure slots for missing device got disconnected and removed
   661  	hpslot, _ = repo.SlotForHotplugKey("test-a", "key-other-device")
   662  	c.Assert(hpslot, IsNil)
   663  
   664  	// and the connection for missing device is marked with hotplug-gone: true;
   665  	// "anotherslot" is removed completely since there was no connection for it.
   666  	var newconns map[string]interface{}
   667  	c.Assert(st.Get("conns", &newconns), IsNil)
   668  	c.Check(newconns, DeepEquals, map[string]interface{}{
   669  		"consumer:plug core:hotplugslot": map[string]interface{}{
   670  			"hotplug-gone": true,
   671  			"hotplug-key":  "key-other-device",
   672  			"interface":    "test-a"}})
   673  
   674  	var newHotplugSlots map[string]interface{}
   675  	c.Assert(st.Get("hotplug-slots", &newHotplugSlots), IsNil)
   676  	c.Check(newHotplugSlots, DeepEquals, map[string]interface{}{
   677  		"hotplugslot-a": map[string]interface{}{
   678  			"interface": "test-a", "hotplug-gone": false, "static-attrs": map[string]interface{}{"slot-a-attr1": "a", "path": di.DevicePath()}, "hotplug-key": "key-1", "name": "hotplugslot-a"},
   679  		"hotplugslot-b": map[string]interface{}{
   680  			"name": "hotplugslot-b", "hotplug-gone": false, "interface": "test-b", "hotplug-key": "key-2"},
   681  		"hotplugslot": map[string]interface{}{"name": "hotplugslot", "hotplug-gone": true, "interface": "test-a", "hotplug-key": "key-other-device"}})
   682  }
   683  
   684  func (s *hotplugSuite) TestHotplugDeviceUpdate(c *C) {
   685  	s.MockModel(c, nil)
   686  	st := s.state
   687  	st.Lock()
   688  
   689  	// existing connection
   690  	st.Set("conns", map[string]interface{}{
   691  		"consumer:plug core:hotplugslot-a": map[string]interface{}{
   692  			"interface":    "test-a",
   693  			"hotplug-key":  "key-1",
   694  			"hotplug-gone": false,
   695  			"slot-static":  map[string]interface{}{"path": "/path-1"}}})
   696  
   697  	repo := s.mgr.Repository()
   698  	si := &snap.SideInfo{RealName: "consumer", Revision: snap.R(1)}
   699  	testSnap := snaptest.MockSnapInstance(c, "", testSnapYaml, si)
   700  	c.Assert(repo.AddPlug(&snap.PlugInfo{
   701  		Interface: "test-a",
   702  		Name:      "plug",
   703  		Attrs:     map[string]interface{}{},
   704  		Snap:      testSnap,
   705  	}), IsNil)
   706  	snapstate.Set(s.state, "consumer", &snapstate.SnapState{
   707  		Active:   true,
   708  		Sequence: []*snap.SideInfo{si},
   709  		Current:  snap.R(1),
   710  		SnapType: "app"})
   711  
   712  	core, err := snapstate.CurrentInfo(s.state, "core")
   713  	c.Assert(err, IsNil)
   714  	c.Assert(repo.AddSlot(&snap.SlotInfo{
   715  		Interface:  "test-a",
   716  		Name:       "hotplugslot-a",
   717  		Attrs:      map[string]interface{}{"path": "/path-1"},
   718  		Snap:       core,
   719  		HotplugKey: "key-1",
   720  	}), IsNil)
   721  
   722  	conn, err := repo.Connect(&interfaces.ConnRef{
   723  		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
   724  		SlotRef: interfaces.SlotRef{Snap: "core", Name: "hotplugslot-a"},
   725  	}, nil, nil, nil, nil, nil)
   726  	c.Assert(err, IsNil)
   727  	c.Assert(conn, NotNil)
   728  
   729  	st.Set("hotplug-slots", map[string]interface{}{
   730  		"hotplugslot-a": map[string]interface{}{
   731  			"name":         "hotplugslot-a",
   732  			"interface":    "test-a",
   733  			"hotplug-key":  "key-1",
   734  			"static-attrs": map[string]interface{}{"path": "/path-1"}}})
   735  	st.Unlock()
   736  
   737  	// simulate device update
   738  	di, err := hotplug.NewHotplugDeviceInfo(map[string]string{"DEVPATH": "a/path", "ACTION": "add", "SUBSYSTEM": "foo"})
   739  	c.Assert(err, IsNil)
   740  	s.udevMon.AddDevice(di)
   741  	s.udevMon.EnumerationDone()
   742  
   743  	c.Assert(s.o.Settle(5*time.Second), IsNil)
   744  
   745  	st.Lock()
   746  	defer st.Unlock()
   747  
   748  	// verify hotplug tasks
   749  	var hp hotplugTasksWitness
   750  	hp.checkTasks(c, s.state)
   751  
   752  	// we see 2 hotplug-connect tasks because of interface test-a and test-b (the latter does nothing as there is no change)
   753  	c.Check(hp.seenTasks, DeepEquals, map[string]int{"hotplug-seq-wait": 2, "hotplug-connect": 2, "hotplug-disconnect": 1, "connect": 1, "disconnect": 1, "hotplug-add-slot": 2, "hotplug-update-slot": 1})
   754  	c.Check(hp.seenHooks, DeepEquals, map[string]string{
   755  		"disconnect-plug-plug": "consumer",
   756  		"prepare-plug-plug":    "consumer",
   757  		"connect-plug-plug":    "consumer"})
   758  	c.Check(hp.hotplugDisconnects, DeepEquals, map[snap.HotplugKey]string{"key-1": "test-a"})
   759  	c.Check(hp.seenHotplugAddKeys, DeepEquals, map[snap.HotplugKey]string{"key-1": "test-a", "key-2": "test-b"})
   760  	c.Check(hp.seenHotplugConnectKeys, DeepEquals, map[snap.HotplugKey]string{"key-1": "test-a", "key-2": "test-b"})
   761  	c.Check(hp.seenHotplugUpdateKeys, DeepEquals, map[snap.HotplugKey]string{"key-1": "test-a"})
   762  	c.Check(hp.connects, DeepEquals, []string{"consumer:plug core:hotplugslot-a"})
   763  	c.Check(hp.disconnects, DeepEquals, []string{"consumer:plug core:hotplugslot-a"})
   764  
   765  	// make sure slots for new device have been updated in the repo
   766  	slot, err := repo.SlotForHotplugKey("test-a", "key-1")
   767  	c.Assert(err, IsNil)
   768  	c.Check(slot.Attrs, DeepEquals, map[string]interface{}{"path": di.DevicePath(), "slot-a-attr1": "a"})
   769  
   770  	// and the connection attributes have been updated
   771  	var newconns map[string]interface{}
   772  	c.Assert(st.Get("conns", &newconns), IsNil)
   773  	c.Check(newconns, DeepEquals, map[string]interface{}{
   774  		"consumer:plug core:hotplugslot-a": map[string]interface{}{
   775  			"hotplug-key": "key-1",
   776  			"interface":   "test-a",
   777  			"slot-static": map[string]interface{}{"path": di.DevicePath(), "slot-a-attr1": "a"},
   778  		}})
   779  
   780  	var newHotplugSlots map[string]interface{}
   781  	c.Assert(st.Get("hotplug-slots", &newHotplugSlots), IsNil)
   782  	c.Check(newHotplugSlots["hotplugslot-a"], DeepEquals, map[string]interface{}{
   783  		"interface":    "test-a",
   784  		"static-attrs": map[string]interface{}{"slot-a-attr1": "a", "path": di.DevicePath()},
   785  		"hotplug-key":  "key-1",
   786  		"name":         "hotplugslot-a",
   787  		"hotplug-gone": false})
   788  }
   789  
   790  func keyHelper(input string) snap.HotplugKey {
   791  	return snap.HotplugKey(fmt.Sprintf("0%x", sha256.Sum256([]byte(input))))
   792  }
   793  
   794  func (s *hotplugSuite) TestDefaultDeviceKey(c *C) {
   795  	di, err := hotplug.NewHotplugDeviceInfo(map[string]string{
   796  		"DEVPATH":        "a/path",
   797  		"ACTION":         "add",
   798  		"SUBSYSTEM":      "foo",
   799  		"ID_V4L_PRODUCT": "v4lproduct",
   800  		"NAME":           "name",
   801  		"ID_VENDOR_ID":   "vendor",
   802  		"ID_MODEL_ID":    "model",
   803  		"ID_SERIAL":      "serial",
   804  		"ID_REVISION":    "revision",
   805  	})
   806  	c.Assert(err, IsNil)
   807  	key, err := ifacestate.DefaultDeviceKey(di, 0)
   808  	c.Assert(err, IsNil)
   809  
   810  	// sanity check
   811  	c.Check(key, HasLen, 65)
   812  	c.Check(key, Equals, snap.HotplugKey("08bcbdcda3fee3534c0288506d9b75d4e26fe3692a36a11e75d05eac9ebf5ca7d"))
   813  	c.Assert(key, Equals, keyHelper("ID_V4L_PRODUCT\x00v4lproduct\x00ID_VENDOR_ID\x00vendor\x00ID_MODEL_ID\x00model\x00ID_SERIAL\x00serial\x00"))
   814  
   815  	di, err = hotplug.NewHotplugDeviceInfo(map[string]string{
   816  		"DEVPATH":      "a/path",
   817  		"ACTION":       "add",
   818  		"SUBSYSTEM":    "foo",
   819  		"NAME":         "name",
   820  		"ID_WWN":       "wnn",
   821  		"ID_MODEL_ENC": "modelenc",
   822  		"ID_REVISION":  "revision",
   823  	})
   824  	c.Assert(err, IsNil)
   825  	key, err = ifacestate.DefaultDeviceKey(di, 0)
   826  	c.Assert(err, IsNil)
   827  	c.Assert(key, Equals, keyHelper("NAME\x00name\x00ID_WWN\x00wnn\x00ID_MODEL_ENC\x00modelenc\x00ID_REVISION\x00revision\x00"))
   828  
   829  	di, err = hotplug.NewHotplugDeviceInfo(map[string]string{
   830  		"DEVPATH":       "a/path",
   831  		"ACTION":        "add",
   832  		"SUBSYSTEM":     "foo",
   833  		"PCI_SLOT_NAME": "pcislot",
   834  		"ID_MODEL_ENC":  "modelenc",
   835  	})
   836  	c.Assert(err, IsNil)
   837  	key, err = ifacestate.DefaultDeviceKey(di, 0)
   838  	c.Assert(key, Equals, keyHelper("PCI_SLOT_NAME\x00pcislot\x00ID_MODEL_ENC\x00modelenc\x00"))
   839  	c.Assert(err, IsNil)
   840  
   841  	// real device #1 - Lime SDR device
   842  	di, err = hotplug.NewHotplugDeviceInfo(map[string]string{
   843  		"DEVNAME":                 "/dev/bus/usb/002/002",
   844  		"DEVNUM":                  "002",
   845  		"DEVPATH":                 "/devices/pci0000:00/0000:00:14.0/usb2/2-3",
   846  		"DEVTYPE":                 "usb_device",
   847  		"DRIVER":                  "usb",
   848  		"ID_BUS":                  "usb",
   849  		"ID_MODEL":                "LimeSDR-USB",
   850  		"ID_MODEL_ENC":            "LimeSDR-USB",
   851  		"ID_MODEL_FROM_DATABASE":  "Myriad-RF LimeSDR",
   852  		"ID_MODEL_ID":             "6108",
   853  		"ID_REVISION":             "0000",
   854  		"ID_SERIAL":               "Myriad-RF_LimeSDR-USB_0009060B00492E2C",
   855  		"ID_SERIAL_SHORT":         "0009060B00492E2C",
   856  		"ID_USB_INTERFACES":       ":ff0000:",
   857  		"ID_VENDOR":               "Myriad-RF",
   858  		"ID_VENDOR_ENC":           "Myriad-RF",
   859  		"ID_VENDOR_FROM_DATABASE": "OpenMoko, Inc.",
   860  		"ID_VENDOR_ID":            "1d50",
   861  		"MAJOR":                   "189",
   862  		"MINOR":                   "129",
   863  		"PRODUCT":                 "1d50/6108/0",
   864  		"SUBSYSTEM":               "usb",
   865  		"TYPE":                    "0/0/0",
   866  		"USEC_INITIALIZED":        "6125378086 ",
   867  	})
   868  	c.Assert(err, IsNil)
   869  	key, err = ifacestate.DefaultDeviceKey(di, 0)
   870  	c.Assert(err, IsNil)
   871  	c.Assert(key, Equals, keyHelper("ID_VENDOR_ID\x001d50\x00ID_MODEL_ID\x006108\x00ID_SERIAL\x00Myriad-RF_LimeSDR-USB_0009060B00492E2C\x00"))
   872  
   873  	// real device #2 - usb-serial port adapter
   874  	di, err = hotplug.NewHotplugDeviceInfo(map[string]string{
   875  		"DEVLINKS":                       "/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_AH06W0EQ-if00-port0 /dev/serial/by-path/pci-0000:00:14.0-usb-0:2:1.0-port0",
   876  		"DEVNAME":                        "/dev/ttyUSB0",
   877  		"DEVPATH":                        "/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2:1.0/ttyUSB0/tty/ttyUSB0",
   878  		"ID_BUS":                         "usb",
   879  		"ID_MM_CANDIDATE":                "1",
   880  		"ID_MODEL_ENC":                   "FT232R\x20USB\x20UART",
   881  		"MODEL_FROM_DATABASE":            "FT232 Serial (UART) IC",
   882  		"ID_MODEL_ID":                    "6001",
   883  		"ID_PATH":                        "pci-0000:00:14.0-usb-0:2:1.0",
   884  		"ID_PATH_TAG":                    "pci-0000_00_14_0-usb-0_2_1_0",
   885  		"ID_PCI_CLASS_FROM_DATABASE":     "Serial bus controller",
   886  		"ID_PCI_INTERFACE_FROM_DATABASE": "XHCI",
   887  		"ID_PCI_SUBCLASS_FROM_DATABASE":  "USB controller",
   888  		"ID_REVISION":                    "0600",
   889  		"ID_SERIAL":                      "FTDI_FT232R_USB_UART_AH06W0EQ",
   890  		"ID_SERIAL_SHORT":                "AH06W0EQ",
   891  		"ID_TYPE":                        "generic",
   892  		"ID_USB_DRIVER":                  "ftdi_sio",
   893  		"ID_USB_INTERFACES":              ":ffffff:",
   894  		"ID_USB_INTERFACE_NUM":           "00",
   895  		"ID_VENDOR":                      "FTDI",
   896  		"ID_VENDOR_ENC":                  "FTDI",
   897  		"ID_VENDOR_FROM_DATABASE":        "Future Technology Devices International, Ltd",
   898  		"ID_VENDOR_ID":                   "0403",
   899  		"MAJOR":                          "188",
   900  		"MINOR":                          "0",
   901  		"SUBSYSTEM":                      "tty",
   902  		"TAGS":                           ":systemd:",
   903  		"USEC_INITIALIZED":               "6571662103",
   904  	})
   905  	c.Assert(err, IsNil)
   906  	key, err = ifacestate.DefaultDeviceKey(di, 0)
   907  	c.Assert(err, IsNil)
   908  	c.Assert(key, Equals, keyHelper("ID_VENDOR_ID\x000403\x00ID_MODEL_ID\x006001\x00ID_SERIAL\x00FTDI_FT232R_USB_UART_AH06W0EQ\x00"))
   909  
   910  	// real device #3 - integrated web camera
   911  	di, err = hotplug.NewHotplugDeviceInfo(map[string]string{
   912  		"COLORD_DEVICE":        "1",
   913  		"COLORD_KIND":          "camera",
   914  		"DEVLINKS":             "/dev/v4l/by-path/pci-0000:00:14.0-usb-0:11:1.0-video-index0 /dev/v4l/by-id/usb-CN0J8NNP7248766FA3H3A01_Integrated_Webcam_HD_200901010001-video-index0",
   915  		"DEVNAME":              "/dev/video0",
   916  		"DEVPATH":              "/devices/pci0000:00/0000:00:14.0/usb1/1-11/1-11:1.0/video4linux/video0",
   917  		"ID_BUS":               "usb",
   918  		"ID_FOR_SEAT":          "video4linux-pci-0000_00_14_0-usb-0_11_1_0",
   919  		"ID_MODEL":             "Integrated_Webcam_HD",
   920  		"ID_MODEL_ENC":         "Integrated_Webcam_HD",
   921  		"ID_MODEL_ID":          "57c3",
   922  		"ID_PATH":              "pci-0000:00:14.0-usb-0:11:1.0",
   923  		"ID_PATH_TAG":          "pci-0000_00_14_0-usb-0_11_1_0",
   924  		"ID_REVISION":          "5806",
   925  		"ID_SERIAL":            "CN0J8NNP7248766FA3H3A01_Integrated_Webcam_HD_200901010001",
   926  		"ID_SERIAL_SHORT":      "200901010001",
   927  		"ID_TYPE":              "video",
   928  		"ID_USB_DRIVER":        "uvcvideo",
   929  		"ID_USB_INTERFACES":    ":0e0100:0e0200:",
   930  		"ID_USB_INTERFACE_NUM": "00",
   931  		"ID_V4L_CAPABILITIES":  ":capture:",
   932  		"ID_V4L_PRODUCT":       "Integrated_Webcam_HD: Integrate",
   933  		"ID_V4L_VERSION":       "2",
   934  		"ID_VENDOR":            "CN0J8NNP7248766FA3H3A01",
   935  		"ID_VENDOR_ENC":        "CN0J8NNP7248766FA3H3A01",
   936  		"ID_VENDOR_ID":         "0bda",
   937  		"MAJOR":                "81",
   938  		"MINOR":                "0",
   939  		"SUBSYSTEM":            "video4linux",
   940  		"TAGS":                 ":uaccess:seat:",
   941  		"USEC_INITIALIZED":     "3411321",
   942  	})
   943  	c.Assert(err, IsNil)
   944  	key, err = ifacestate.DefaultDeviceKey(di, 0)
   945  	c.Assert(err, IsNil)
   946  	c.Assert(key, Equals, keyHelper("ID_V4L_PRODUCT\x00Integrated_Webcam_HD: Integrate\x00ID_VENDOR_ID\x000bda\x00ID_MODEL_ID\x0057c3\x00ID_SERIAL\x00CN0J8NNP7248766FA3H3A01_Integrated_Webcam_HD_200901010001\x00"))
   947  
   948  	// key cannot be computed - empty string
   949  	di, err = hotplug.NewHotplugDeviceInfo(map[string]string{
   950  		"DEVPATH":   "a/path",
   951  		"ACTION":    "add",
   952  		"SUBSYSTEM": "foo",
   953  	})
   954  	c.Assert(err, IsNil)
   955  	key, err = ifacestate.DefaultDeviceKey(di, 0)
   956  	c.Assert(err, IsNil)
   957  	c.Assert(key, Equals, snap.HotplugKey(""))
   958  }
   959  
   960  func (s *hotplugSuite) TestDefaultDeviceKeyError(c *C) {
   961  	di, err := hotplug.NewHotplugDeviceInfo(map[string]string{
   962  		"DEVPATH":      "a/path",
   963  		"ACTION":       "add",
   964  		"SUBSYSTEM":    "foo",
   965  		"NAME":         "name",
   966  		"ID_VENDOR_ID": "vendor",
   967  		"ID_MODEL_ID":  "model",
   968  		"ID_SERIAL":    "serial",
   969  	})
   970  	c.Assert(err, IsNil)
   971  	_, err = ifacestate.DefaultDeviceKey(di, 16)
   972  	c.Assert(err, ErrorMatches, "internal error: invalid key version 16")
   973  }
   974  
   975  func (s *hotplugSuite) TestEnsureUniqueName(c *C) {
   976  	fakeRepositoryLookup := func(n string) bool {
   977  		reserved := map[string]bool{
   978  			"slot1":    true,
   979  			"slot":     true,
   980  			"slot1234": true,
   981  			"slot-1":   true,
   982  		}
   983  		return !reserved[n]
   984  	}
   985  
   986  	names := []struct{ proposedName, resultingName string }{
   987  		{"foo", "foo"},
   988  		{"slot", "slot-2"},
   989  		{"slot1234", "slot1234-1"},
   990  		{"slot-1", "slot-1-1"},
   991  	}
   992  
   993  	for _, name := range names {
   994  		c.Assert(ifacestate.EnsureUniqueName(name.proposedName, fakeRepositoryLookup), Equals, name.resultingName)
   995  	}
   996  }
   997  
   998  func (s *hotplugSuite) TestMakeSlotName(c *C) {
   999  	names := []struct{ proposedName, resultingName string }{
  1000  		{"", ""},
  1001  		{"-", ""},
  1002  		{"slot1", "slot1"},
  1003  		{"-slot1", "slot1"},
  1004  		{"a--slot-1", "a-slot-1"},
  1005  		{"(-slot", "slot"},
  1006  		{"(--slot", "slot"},
  1007  		{"slot-", "slot"},
  1008  		{"slot---", "slot"},
  1009  		{"slot-(", "slot"},
  1010  		{"Integrated_Webcam_HD", "integratedwebcamhd"},
  1011  		{"Xeon E3-1200 v5/E3-1500 v5/6th Gen Core Processor Host Bridge/DRAM Registers", "xeone3-1200v5e3-1500"},
  1012  	}
  1013  	for _, name := range names {
  1014  		c.Assert(ifacestate.MakeSlotName(name.proposedName), Equals, name.resultingName)
  1015  	}
  1016  }
  1017  
  1018  func (s *hotplugSuite) TestSuggestedSlotName(c *C) {
  1019  
  1020  	events := []struct {
  1021  		eventData map[string]string
  1022  		outName   string
  1023  	}{{
  1024  		map[string]string{
  1025  			"DEVPATH":                "a/path",
  1026  			"ACTION":                 "add",
  1027  			"SUBSYSTEM":              "foo",
  1028  			"NAME":                   "Name",
  1029  			"ID_MODEL":               "Longer Name",
  1030  			"ID_MODEL_FROM_DATABASE": "Longest Name",
  1031  		},
  1032  		"name",
  1033  	}, {
  1034  		map[string]string{
  1035  			"DEVPATH":                "a/path",
  1036  			"ACTION":                 "add",
  1037  			"SUBSYSTEM":              "foo",
  1038  			"ID_MODEL":               "Longer Name",
  1039  			"ID_MODEL_FROM_DATABASE": "Longest Name",
  1040  		},
  1041  		"longername",
  1042  	}, {
  1043  		map[string]string{
  1044  			"DEVPATH":                "a/path",
  1045  			"ACTION":                 "add",
  1046  			"SUBSYSTEM":              "foo",
  1047  			"ID_MODEL_FROM_DATABASE": "Longest Name",
  1048  		},
  1049  		"longestname",
  1050  	}, {
  1051  		map[string]string{
  1052  			"DEVPATH":   "a/path",
  1053  			"ACTION":    "add",
  1054  			"SUBSYSTEM": "foo",
  1055  		},
  1056  		"fallbackname",
  1057  	},
  1058  	}
  1059  
  1060  	for _, data := range events {
  1061  		di, err := hotplug.NewHotplugDeviceInfo(data.eventData)
  1062  		c.Assert(err, IsNil)
  1063  
  1064  		slotName := ifacestate.SuggestedSlotName(di, "fallbackname")
  1065  		c.Assert(slotName, Equals, data.outName)
  1066  	}
  1067  }
  1068  
  1069  func (s *hotplugSuite) TestHotplugSlotName(c *C) {
  1070  	st := state.New(nil)
  1071  	st.Lock()
  1072  	defer st.Unlock()
  1073  
  1074  	testData := []struct {
  1075  		slotSpecName string
  1076  		deviceData   map[string]string
  1077  		expectedName string
  1078  	}{
  1079  		// names dervied from slotSpecName
  1080  		{"hdcamera", map[string]string{"DEVPATH": "a", "NAME": "Video Camera"}, "hdcamera"},
  1081  		{"hdcamera", map[string]string{"DEVPATH": "a", "NAME": "Video Camera"}, "hdcamera-1"},
  1082  		{"ieee1394", map[string]string{"DEVPATH": "a"}, "ieee1394"},
  1083  		{"ieee1394", map[string]string{"DEVPATH": "b"}, "ieee1394-1"},
  1084  		{"ieee1394", map[string]string{"DEVPATH": "c"}, "ieee1394-2"},
  1085  		// names derived from device attributes, since slotSpecName is empty
  1086  		{"", map[string]string{"DEVPATH": "a", "NAME": "Video Camera"}, "videocamera"},
  1087  		{"", map[string]string{"DEVPATH": "b", "NAME": "Video Camera"}, "videocamera-1"},
  1088  		{"", map[string]string{"DEVPATH": "b", "NAME": "Video Camera"}, "videocamera-2"},
  1089  		// names derived from interface name, since slotSpecName and relevant device attributes are not present
  1090  		{"", map[string]string{"DEVPATH": "a"}, "ifacename"},
  1091  		{"", map[string]string{"DEVPATH": "a"}, "ifacename-1"},
  1092  	}
  1093  
  1094  	repo := interfaces.NewRepository()
  1095  	iface := &ifacetest.TestInterface{InterfaceName: "camera"}
  1096  	repo.AddInterface(iface)
  1097  
  1098  	stateSlots, err := ifacestate.GetHotplugSlots(st)
  1099  	c.Assert(err, IsNil)
  1100  
  1101  	for _, data := range testData {
  1102  		devinfo, err := hotplug.NewHotplugDeviceInfo(data.deviceData)
  1103  		c.Assert(err, IsNil)
  1104  		c.Check(ifacestate.HotplugSlotName("key", "core", data.slotSpecName, "ifacename", devinfo, repo, stateSlots), Equals, data.expectedName)
  1105  		// store the slot to affect ensureUniqueName
  1106  		stateSlots[data.expectedName] = &ifacestate.HotplugSlotInfo{}
  1107  	}
  1108  }
  1109  
  1110  func (s *hotplugSuite) TestUpdateDeviceTasks(c *C) {
  1111  	st := state.New(nil)
  1112  	st.Lock()
  1113  	defer st.Unlock()
  1114  
  1115  	tss := ifacestate.UpdateDevice(st, "interface", "key", map[string]interface{}{"attr": "value"})
  1116  	c.Assert(tss, NotNil)
  1117  	c.Assert(tss.Tasks(), HasLen, 2)
  1118  
  1119  	task1 := tss.Tasks()[0]
  1120  	c.Assert(task1.Kind(), Equals, "hotplug-disconnect")
  1121  	testHotplugTaskAttrs(c, task1, "interface", "key")
  1122  
  1123  	task2 := tss.Tasks()[1]
  1124  	c.Assert(task2.Kind(), Equals, "hotplug-update-slot")
  1125  	testHotplugTaskAttrs(c, task2, "interface", "key")
  1126  
  1127  	var attrs map[string]interface{}
  1128  	c.Assert(task2.Get("slot-attrs", &attrs), IsNil)
  1129  	c.Assert(attrs, DeepEquals, map[string]interface{}{"attr": "value"})
  1130  
  1131  	wt := task2.WaitTasks()
  1132  	c.Assert(wt, HasLen, 1)
  1133  	c.Assert(wt[0], DeepEquals, task1)
  1134  }
  1135  
  1136  func (s *hotplugSuite) TestRemoveDeviceTasks(c *C) {
  1137  	st := state.New(nil)
  1138  	st.Lock()
  1139  	defer st.Unlock()
  1140  
  1141  	tss := ifacestate.RemoveDevice(st, "interface", "key")
  1142  	c.Assert(tss, NotNil)
  1143  	c.Assert(tss.Tasks(), HasLen, 2)
  1144  
  1145  	task1 := tss.Tasks()[0]
  1146  	c.Assert(task1.Kind(), Equals, "hotplug-disconnect")
  1147  	testHotplugTaskAttrs(c, task1, "interface", "key")
  1148  
  1149  	task2 := tss.Tasks()[1]
  1150  	c.Assert(task2.Kind(), Equals, "hotplug-remove-slot")
  1151  	testHotplugTaskAttrs(c, task2, "interface", "key")
  1152  
  1153  	wt := task2.WaitTasks()
  1154  	c.Assert(wt, HasLen, 1)
  1155  	c.Assert(wt[0], DeepEquals, task1)
  1156  }