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