Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

interfaces/hotplug: add hotplug Specification and HotplugDeviceInfo #5416

Merged
merged 22 commits into from
Jul 5, 2018
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
275f7ad
Added hotplug Specification and HotplugDeviceInfo.
stolowski Jun 27, 2018
280ed48
Merge branch 'master' into hotplug-device-info
stolowski Jun 28, 2018
8163720
Merge branch 'master' into hotplug-device-info
stolowski Jun 28, 2018
1edbf70
Review feedback.
stolowski Jun 28, 2018
9a5f9da
Removed type HotplugDeviceHandler type definition for now.
stolowski Jun 28, 2018
63b33d0
Updated HotPlugSpec.
stolowski Jun 28, 2018
07adf32
Merge branch 'master' into hotplug-device-info
stolowski Jun 28, 2018
7dd7efe
Merge branch 'master' into hotplug-device-info
stolowski Jun 29, 2018
e408116
Merge branch 'master' into hotplug-device-info
stolowski Jun 29, 2018
e3adc77
Merge branch 'master' into hotplug-device-info
stolowski Jun 29, 2018
23e8ec6
Merge branch 'master' into hotplug-device-info
stolowski Jun 29, 2018
06eb95c
Sort slots.
stolowski Jun 29, 2018
4c637dd
Review comments.
stolowski Jun 29, 2018
4c23113
More comments.
stolowski Jun 29, 2018
ee9c8b6
Made data private, added generic Attribute getter.
stolowski Jun 29, 2018
dc6a005
Added Definer type.
stolowski Jul 2, 2018
4bff39a
Merge branch 'master' into hotplug-device-info
stolowski Jul 2, 2018
7ceb603
Merge branch 'master' into hotplug-device-info
stolowski Jul 4, 2018
9c027cf
Validate interface name in the hotplug spec.
stolowski Jul 4, 2018
8d1b22d
Drop Object attribute from HotplugDeviceInfo. Use snap.ValidateSlotName.
stolowski Jul 5, 2018
00276aa
Reverted the addition of ValidateName.
stolowski Jul 5, 2018
237c82b
Check that DEVPATH exists.
stolowski Jul 5, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dirs/dirs.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ var (
SnapshotsDir string

ErrtrackerDbDir string
SysfsDir string
)

const (
Expand Down Expand Up @@ -267,4 +268,5 @@ func SetRootDir(rootdir string) {
SnapshotsDir = filepath.Join(rootdir, snappyDir, "snapshots")

ErrtrackerDbDir = filepath.Join(rootdir, snappyDir, "errtracker.db")
SysfsDir = filepath.Join(rootdir, "/sys")
}
36 changes: 6 additions & 30 deletions interfaces/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func getAttribute(snapName string, ifaceName string, staticAttrs map[string]inte
func NewConnectedSlot(slot *snap.SlotInfo, dynamicAttrs map[string]interface{}) *ConnectedSlot {
return &ConnectedSlot{
slotInfo: slot,
staticAttrs: copyAttributes(slot.Attrs),
staticAttrs: utils.CopyAttributes(slot.Attrs),
dynamicAttrs: utils.NormalizeInterfaceAttributes(dynamicAttrs).(map[string]interface{}),
}
}
Expand All @@ -115,7 +115,7 @@ func NewConnectedSlot(slot *snap.SlotInfo, dynamicAttrs map[string]interface{})
func NewConnectedPlug(plug *snap.PlugInfo, dynamicAttrs map[string]interface{}) *ConnectedPlug {
return &ConnectedPlug{
plugInfo: plug,
staticAttrs: copyAttributes(plug.Attrs),
staticAttrs: utils.CopyAttributes(plug.Attrs),
dynamicAttrs: utils.NormalizeInterfaceAttributes(dynamicAttrs).(map[string]interface{}),
}
}
Expand Down Expand Up @@ -157,12 +157,12 @@ func (plug *ConnectedPlug) StaticAttr(key string, val interface{}) error {

// StaticAttrs returns all static attributes.
func (plug *ConnectedPlug) StaticAttrs() map[string]interface{} {
return copyAttributes(plug.staticAttrs)
return utils.CopyAttributes(plug.staticAttrs)
}

// DynamicAttrs returns all dynamic attributes.
func (plug *ConnectedPlug) DynamicAttrs() map[string]interface{} {
return copyAttributes(plug.dynamicAttrs)
return utils.CopyAttributes(plug.dynamicAttrs)
}

// Attr returns a dynamic attribute with the given name. It falls back to returning static
Expand Down Expand Up @@ -230,12 +230,12 @@ func (slot *ConnectedSlot) StaticAttr(key string, val interface{}) error {

// StaticAttrs returns all static attributes.
func (slot *ConnectedSlot) StaticAttrs() map[string]interface{} {
return copyAttributes(slot.staticAttrs)
return utils.CopyAttributes(slot.staticAttrs)
}

// DynamicAttrs returns all dynamic attributes.
func (slot *ConnectedSlot) DynamicAttrs() map[string]interface{} {
return copyAttributes(slot.dynamicAttrs)
return utils.CopyAttributes(slot.dynamicAttrs)
}

// Attr returns a dynamic attribute with the given name. It falls back to returning static
Expand Down Expand Up @@ -270,27 +270,3 @@ func (slot *ConnectedSlot) Ref() *SlotRef {
func (conn *Connection) Interface() string {
return conn.Plug.plugInfo.Interface
}

func copyAttributes(value map[string]interface{}) map[string]interface{} {
return copyRecursive(value).(map[string]interface{})
}

func copyRecursive(value interface{}) interface{} {
// note: ensure all the mutable types (or types that need a conversion)
// are handled here.
switch v := value.(type) {
case []interface{}:
arr := make([]interface{}, len(v))
for i, el := range v {
arr[i] = copyRecursive(el)
}
return arr
case map[string]interface{}:
mp := make(map[string]interface{}, len(v))
for key, item := range v {
mp[key] = copyRecursive(item)
}
return mp
}
return value
}
3 changes: 2 additions & 1 deletion interfaces/connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package interfaces
import (
. "gopkg.in/check.v1"

"github.com/snapcore/snapd/interfaces/utils"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/testutil"
Expand Down Expand Up @@ -310,7 +311,7 @@ func (s *connSuite) TestCopyAttributes(c *C) {
"e": map[string]interface{}{"e1": "E1"},
}

cpy := CopyAttributes(orig)
cpy := utils.CopyAttributes(orig)
c.Check(cpy, DeepEquals, orig)

cpy["d"].([]interface{})[0] = 999
Expand Down
3 changes: 1 addition & 2 deletions interfaces/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ func (c BySlotInfo) Swap(i, j int) { bySlotInfo(c).Swap(i, j) }
func (c BySlotInfo) Less(i, j int) bool { return bySlotInfo(c).Less(i, j) }

var (
CopyAttributes = copyAttributes
FindSnapdPath = findSnapdPath
FindSnapdPath = findSnapdPath
)

// MockIsHomeUsingNFS mocks the real implementation of osutil.IsHomeUsingNFS
Expand Down
85 changes: 85 additions & 0 deletions interfaces/hotplug/deviceinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package hotplug

import (
"path/filepath"

"github.com/snapcore/snapd/dirs"
)

// HotplugDeviceInfo carries information about added/removed device detected at runtime.
type HotplugDeviceInfo struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document this somehow.

HotplugDeviceInfo stores information about devices added dynamically at runtime.

// map of all attributes returned for given uevent.
data map[string]string
}

// NewHotplugDeviceInfo creates HotplugDeviceInfo structure related to udev add or remove event.
func NewHotplugDeviceInfo(env map[string]string) *HotplugDeviceInfo {
return &HotplugDeviceInfo{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a sanity check this should ensure that env contains DEVPATH.

data: env,
}
}

// Returns the value of "SUBSYSTEM" attribute of the udev event associated with the device, e.g. "usb".
// Subsystem value is always present.
func (h *HotplugDeviceInfo) Subsystem() string {
return h.data["SUBSYSTEM"]
}

// Returns full device path under /sysfs, e.g /sys/devices/pci0000:00/0000:00:14.0/usb1/1-2.
// The path is derived from DEVPATH attribute of the udev event.
func (h *HotplugDeviceInfo) DevicePath() string {
path, ok := h.Attribute("DEVPATH")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check doesn't do any harm, but Isn't DEVPATH one of the attributes that's supposed to be there always?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have Path and Object as separate functions if they must always return the exact same value?

if ok {
return filepath.Join(dirs.SysfsDir, path)
}
return ""
}

// Returns the value of "MINOR" attribute of the udev event associated with the device.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please indicate that this and everything below is optional and may return an empty string.
I also have a gut feeling Data should be private but we can see how this works.

EDIT: I have a feeling we should just expose an attribute getter and not have APIs for things that are optional (and by looking at udev database, mostly absent).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments. Made data private and added a generic attribute getter. I'm not sure about removing getters for optional attributes, I think these are actually the attributes that are the most interesting for interfaces (even if optional), so having exposed helpers for them feels handy and I'd personally prefer to read di.Minor() than di.Attribute("MINOR").

// The Minor value may be empty.
func (h *HotplugDeviceInfo) Minor() string {
return h.data["MINOR"]
}

// Returns the value of "MAJOR" attribute of the udev event associated with the device.
// The Major value may be empty.
func (h *HotplugDeviceInfo) Major() string {
return h.data["MAJOR"]
}

// Returns the value of "DEVNAME" attribute of the udev event associated with the device, e.g. "ttyUSB0".
// The DeviceName value may be empty.
func (h *HotplugDeviceInfo) DeviceName() string {
return h.data["DEVNAME"]
}

// Returns the value of "DEVTYPE" attribute of the udev event associated with the device, e.g. "usb_device".
// The DeviceType value may be empty.
func (h *HotplugDeviceInfo) DeviceType() string {
return h.data["DEVTYPE"]
}

// Generic method for getting arbitrary attribute from the uevent data.
func (h *HotplugDeviceInfo) Attribute(name string) (string, bool) {
val, ok := h.data[name]
return val, ok
}
92 changes: 92 additions & 0 deletions interfaces/hotplug/deviceinfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package hotplug

import (
"path/filepath"
"testing"

. "gopkg.in/check.v1"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/testutil"
)

func Test(t *testing.T) { TestingT(t) }

type hotplugSuite struct {
testutil.BaseTest
}

var _ = Suite(&hotplugSuite{})

func (s *hotplugSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
dirs.SetRootDir(c.MkDir())
}

func (s *hotplugSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
dirs.SetRootDir("")
}

func (s *hotplugSuite) TestBasicProperties(c *C) {
env := map[string]string{
"DEVPATH": "/devices/pci0000:00/0000:00:14.0/usb2/2-3", "DEVNAME": "bus/usb/002/003",
"DEVTYPE": "usb_device",
"PRODUCT": "1d50/6108/0", "DEVNUM": "003",
"SEQNUM": "4053",
"ACTION": "add", "SUBSYSTEM": "usb",
"MAJOR": "189", "MINOR": "130",
"TYPE": "0/0/0", "BUSNUM": "002",
}

di := NewHotplugDeviceInfo(env)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test with all of the optional things absent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

c.Assert(di.DeviceName(), Equals, "bus/usb/002/003")
c.Assert(di.DeviceType(), Equals, "usb_device")
c.Assert(di.DevicePath(), Equals, filepath.Join(dirs.SysfsDir, "/devices/pci0000:00/0000:00:14.0/usb2/2-3"))
c.Assert(di.Subsystem(), Equals, "usb")
c.Assert(di.Major(), Equals, "189")
c.Assert(di.Minor(), Equals, "130")

minor, ok := di.Attribute("MINOR")
c.Assert(ok, Equals, true)
c.Assert(minor, Equals, "130")

_, ok = di.Attribute("FOO")
c.Assert(ok, Equals, false)
}

func (s *hotplugSuite) TestPropertiesMissing(c *C) {
env := map[string]string{
"DEVPATH": "/devices/pci0000:00/0000:00:14.0/usb2/2-3",
"ACTION": "add", "SUBSYSTEM": "usb",
}

di := NewHotplugDeviceInfo(env)

c.Assert(di.DeviceName(), Equals, "")
c.Assert(di.DeviceType(), Equals, "")
c.Assert(di.DevicePath(), Equals, filepath.Join(dirs.SysfsDir, "/devices/pci0000:00/0000:00:14.0/usb2/2-3"))
c.Assert(di.Subsystem(), Equals, "usb")
c.Assert(di.Major(), Equals, "")
c.Assert(di.Minor(), Equals, "")
}
93 changes: 93 additions & 0 deletions interfaces/hotplug/spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package hotplug

import (
"fmt"
"sort"

"github.com/snapcore/snapd/interfaces/utils"
"github.com/snapcore/snapd/snap"
)

// Definer can be implemented by interfaces that need to create slots in response to hotplug events
type Definer interface {
HotplugDeviceDetected(di *HotplugDeviceInfo, spec *Specification) error
}

// SlotSpec is a definition of the slot to create in response to udev event.
type SlotSpec struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document public things please. This helps you and your reviewers alike.

// XXX: Name is the name the interface wants to give to the slot; we
// might want to mediate this though (e.g. generate automatically), so this
// may change/go away.
Name string
Label string
Attrs map[string]interface{}
}

// Specification contains data about all slots that a hotplug interface wants to have created in response to uevent.
type Specification struct {
// slots are indexed by slot name to ensure unique names
slots map[string]*SlotSpec
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document what the key in the map is. Is it DEVPATH?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented.

}

// NewSpecification creates an empty hotplug Specification.
func NewSpecification() *Specification {
return &Specification{
slots: make(map[string]*SlotSpec),
}
}

// AddSlot adds a specification of a slot.
func (h *Specification) AddSlot(slotSpec *SlotSpec) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must absolutely associate with a sysfs device. How is this being ensured?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to happen outside in the hotplug event processing loop. Hotplug event handler will iterate over all hotplug interfaces, determine device key, create a Specification per-device. Interface fills in the Specification by adding slot(s), the outher code knows device key for given set of slots.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this response as a comment please.

if _, ok := h.slots[slotSpec.Name]; ok {
return fmt.Errorf("slot %q already exists", slotSpec.Name)
}
if err := snap.ValidateSlotName(slotSpec.Name); err != nil {
return err
}
attrs := slotSpec.Attrs
if attrs == nil {
attrs = make(map[string]interface{})
} else {
attrs = utils.CopyAttributes(slotSpec.Attrs)
}
h.slots[slotSpec.Name] = &SlotSpec{
Name: slotSpec.Name,
Label: slotSpec.Label,
Attrs: utils.NormalizeInterfaceAttributes(attrs).(map[string]interface{}),
}
return nil
}

// Slots returns specifications of all slots created by given interface.
func (h *Specification) Slots() []*SlotSpec {
keys := make([]string, 0, len(h.slots))
for k := range h.slots {
keys = append(keys, k)
}
sort.Strings(keys)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these need to be sorted or is it just to get a stable output for the tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's about making it test friendly only.


slots := make([]*SlotSpec, 0, len(h.slots))
for _, k := range keys {
slots = append(slots, h.slots[k])
}
return slots
}
Loading