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

Conversation

stolowski
Copy link
Contributor

Introduce two structures related to hotplug: Specification will be passed to interfaces that implement HotplugDeviceDetected(..) method, it's a way for interfaces to add slot definitions on hot plug add event. HotplugDeviceInfo wraps some uevent data, it will be passed from udev monitor to interface manager callback on hotplug add/remove events.

@zyga
Copy link
Contributor

zyga commented Jun 27, 2018

This is missing some stuff

src/github.com/snapcore/snapd/interfaces/hotplug/spec.go:25:2: cannot find package "github.com/snapcore/snapd/interfaces/utils" in any of:
	/home/gopath/src/github.com/snapcore/snapd/_build/src/github.com/snapcore/snapd/vendor/github.com/snapcore/snapd/interfaces/utils (vendor tree)

Copy link
Contributor

@zyga zyga left a comment

Choose a reason for hiding this comment

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

First pass, -1 because of the issue with make, please ping me for 2nd pass.

dirs/dirs.go Outdated
@@ -106,6 +106,7 @@ var (
SnapshotsDir string

ErrtrackerDbDir string
SysDir string
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick, to avoid any shade of confusion could you please call this SysfsDir instead?

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

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.

type HotplugDeviceInfo struct {
object string
Data map[string]string
idVendor string
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm somewhat -0.5 on keeping those explicit. Instead we could have methods that fish them out of Data

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note, these attributes are not present in Data, they were read lazily on first access from the filesystem. However, removed all the idVendor/idProduct/serial aspects for now per our discussion.

)

type HotplugDeviceInfo struct {
object string
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 you mean by object here. Is this is a sysfs path?

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 a comment to Object() getter.

serial string
}

func NewHotplugDeviceInfo(obj string, env map[string]string) *HotplugDeviceInfo {
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 docs for everything.

HotplugDeviceDetected(di *HotplugDeviceInfo, spec *Specification) error
}

// HotplugDeviceInfo can be implemented by interfaces that need to provide a non-standard device key for hotplug devices
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 a chunk of text right at the top of the file that documents the key concepts of hot plug in snapd. Please specifically document the notion of device key.

}
}

func (h *Specification) AddSlot(name, label string, attrs map[string]interface{}) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this take a SlotInfo instead (and ensure some things are not wired, i.e. no snap.Info). This is more future proof than a function with three arguments.

Copy link
Contributor Author

@stolowski stolowski Jun 28, 2018

Choose a reason for hiding this comment

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

It could, my only concern is there is actually more stuff in SlotInfo that should not be touched/initialized by the interface implementation. We can of course make sure it's not wired (error out or ignore/reset snap.Info, hooks, apps etc.) but it feels like somewhat confusing API. How about something like:
AddSlot(slot SlotSpec)
so this is stable and future-proof, SlotSpec has only the attributes/setters that are relevant and we have clear way of mediating.

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

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.

}

type Specification struct {
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.

I would make this map[string]*SlotSpec as there are maps inside SlotSpec and making all of this by-value feels bad.

}

func (h *Specification) Slots() []SlotSpec {
slots := make([]SlotSpec, len(h.slots))
Copy link
Contributor

Choose a reason for hiding this comment

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

This is wrong, you meant make([]SlotSpec, 0, len(h.slots))

Probably implies this is not tested.

Copy link
Contributor

@bboozzoo bboozzoo left a comment

Choose a reason for hiding this comment

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

@zyga covered the rest


func (h *HotplugDeviceInfo) readOnceMaybe(fileName string, out *string) string {
if *out == "" {
data, err := ioutil.ReadFile(filepath.Join(h.Path(), fileName))
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be converted to a public ReadAttribute() (string, error) helper

@codecov-io
Copy link

codecov-io commented Jun 29, 2018

Codecov Report

Merging #5416 into master will increase coverage by 0.08%.
The diff coverage is 74.64%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #5416      +/-   ##
==========================================
+ Coverage   78.92%   79.01%   +0.08%     
==========================================
  Files         509      512       +3     
  Lines       38445    38672     +227     
==========================================
+ Hits        30343    30555     +212     
- Misses       5655     5664       +9     
- Partials     2447     2453       +6
Impacted Files Coverage Δ
interfaces/utils/utils.go 0% <0%> (ø) ⬆️
dirs/dirs.go 97.8% <100%> (+0.02%) ⬆️
interfaces/connection.go 82.52% <100%> (-2.23%) ⬇️
interfaces/hotplug/deviceinfo.go 100% <100%> (ø)
interfaces/hotplug/spec.go 88.88% <88.88%> (ø)
interfaces/policy/policy.go 93.6% <0%> (-3.21%) ⬇️
snap/snaptest/snaptest.go 66.11% <0%> (-2.85%) ⬇️
store/errors.go 77.17% <0%> (-1.31%) ⬇️
overlord/hookstate/hookmgr.go 74.27% <0%> (-0.98%) ⬇️
client/client.go 80% <0%> (ø) ⬆️
... and 9 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 34562bc...237c82b. Read the comment docs.

Copy link
Contributor

@zyga zyga left a comment

Choose a reason for hiding this comment

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

One more pass with some comments. I think this needs some more tweaks before landing (at least in my eyes)


// HotplugDeviceInfo carries information about added/removed device detected at runtime.
type HotplugDeviceInfo struct {
object string
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 just call this devpath please, here and everywhere in the hot plug code. My desire here is to limit the vocabulary used so that we don't invent names for existing kernel constructs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm, I could, however this corresponds to KObj (kernel object?) value associated with the udev event reported by go-udev (what I get from go-udev is kobj string and map of extra data such as DEVPATH etc). While there seems to be direct relation between kobj and DEVPATH, I'm not 100% sure it's always the case, so perhaps it's ok to leave the doors open in case we find both devpath and object values useful?

Copy link
Contributor

Choose a reason for hiding this comment

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

Either devpath and kobjsomething are better than object which is as generic as one can get. Whatever you do please explain what the value is in a comment.

}

// Returns the value of "SUBSYSTEM" attribute of the udev event associated with the device, e.g. "usb".
func (h *HotplugDeviceInfo) Subsystem() string {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it is worth to indicate that SUBSYSTEM is always present. while pretty much everything else is entirely optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Plugging a device in results in multiple add events with generic events first, followed by more concrete ones where you actually got more interesting attributes present. I expect interfaces to filter out (ignore) some events and only react on those that actually cary all important attributes.
Point taken thouhg, I added comments to all attributes.

return h.Data["SUBSYSTEM"]
}

// 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").

}

di := NewHotplugDeviceInfo("/devices/pci0000:00/0000:00:14.0/usb2/2-3", 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.

}

type Specification struct {
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.

}
}

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.

func (s *hotplugSpecSuite) TestAddSlot(c *C) {
spec := NewSpecification()
c.Assert(spec.AddSlot(&SlotSpec{Name: "slot1", Label: "A slot", Attrs: map[string]interface{}{"foo": "bar"}}), IsNil)
c.Assert(spec.AddSlot(&SlotSpec{Name: "slot2", Label: "A slot", Attrs: map[string]interface{}{"baz": "booze"}}), IsNil)
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

foo bar froz bot AFAIK ;-)

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.

// 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) Path() 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?

Copy link
Contributor

@zyga zyga left a comment

Choose a reason for hiding this comment

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

One more pass, let's chat on IRC


// Returns object path, i.e. the sysfs path of the device, e.g. /devices/pci0000:00/0000:00:14.0/usb1/1-2.
// It is expected to be equal to DEVPATH attribute.
func (h *HotplugDeviceInfo) Object() string {
Copy link
Contributor

Choose a reason for hiding this comment

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

I really think this should be just DevPath or Devpath and that we should not use the un-decorated word object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, removed object (and Object getter) alltogether for now, per IRC discussion.

// 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) Path() 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.

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

var validName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$")

// ValidateName checks if a string can be used as a plug or slot name.
func ValidateName(name string) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please drop this and look at snap.Validate{Plug,Slot}Name instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great idea, thanks. Done.

Copy link
Contributor

@zyga zyga left a comment

Choose a reason for hiding this comment

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

LGTM with one last comment


// 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.

@stolowski stolowski merged commit 7982e02 into canonical:master Jul 5, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants