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