The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/2261
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === I've tested all the cases I can think of (hotplug the actual device, hotplug the container config, and cold container start/stop), but I'm not really sure how to add any automated tests for this, since there's no real way to ensure USB devices will be available. Closes #2241 Signed-off-by: Tycho Andersen <tycho.ander...@canonical.com>
From e9a4c9f610fac2f11d5b34fade2d42a256bec3fc Mon Sep 17 00:00:00 2001 From: Tycho Andersen <tycho.ander...@canonical.com> Date: Tue, 2 Aug 2016 13:05:12 -0600 Subject: [PATCH] initial implementation of the "usb" device type I've tested all the cases I can think of (hotplug the actual device, hotplug the container config, and cold container start/stop), but I'm not really sure how to add any automated tests for this, since there's no real way to ensure USB devices will be available. Closes #2241 Signed-off-by: Tycho Andersen <tycho.ander...@canonical.com> --- doc/configuration.md | 14 +++ lxd/api_1.0.go | 1 + lxd/container.go | 21 ++++- lxd/container_lxc.go | 88 +++++++++++++++++++ lxd/db_devices.go | 4 + lxd/devices.go | 238 +++++++++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 358 insertions(+), 8 deletions(-) diff --git a/doc/configuration.md b/doc/configuration.md index 35d4647..3a56f93 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -256,6 +256,20 @@ uid | int | 0 | no | UID of the device owne gid | int | 0 | no | GID of the device owner in the container mode | int | 0660 | no | Mode of the device in the container +### Type: usb +USB device entries simply make the requested USB device appear in the +container. + +The following properties exist: + +Key | Type | Default | Required | Description +:-- | :-- | :-- | :-- | :-- +productid | string | - | yes | The product id of the USB device. +vendorid | string | - | no | The vendor id of the USB device. +uid | int | 0 | no | UID of the device owner in the container +gid | int | 0 | no | GID of the device owner in the container +mode | int | 0660 | no | Mode of the device in the container + ## Profiles Profiles can store any configuration that a container can (key/value or devices) and any number of profiles can be applied to a container. diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go index c02d810..5d6a141 100644 --- a/lxd/api_1.0.go +++ b/lxd/api_1.0.go @@ -62,6 +62,7 @@ func api10Get(d *Daemon, r *http.Request) Response { "container_last_used_at", "etag", "patch", + "usb_devices", }, "api_status": "stable", diff --git a/lxd/container.go b/lxd/container.go index a602ce1..04932db 100644 --- a/lxd/container.go +++ b/lxd/container.go @@ -128,6 +128,21 @@ func containerValidDeviceConfigKey(t, k string) bool { default: return false } + case "usb": + switch k { + case "vendorid": + return true + case "productid": + return true + case "mode": + return true + case "gid": + return true + case "uid": + return true + default: + return false + } case "none": return false default: @@ -180,7 +195,7 @@ func containerValidDevices(devices shared.Devices, profile bool, expanded bool) return fmt.Errorf("Missing device type for device '%s'", name) } - if !shared.StringInSlice(m["type"], []string{"none", "nic", "disk", "unix-char", "unix-block"}) { + if !shared.StringInSlice(m["type"], []string{"none", "nic", "disk", "unix-char", "unix-block", "usb"}) { return fmt.Errorf("Invalid device type for device '%s'", name) } @@ -226,6 +241,10 @@ func containerValidDevices(devices shared.Devices, profile bool, expanded bool) if m["path"] == "" { return fmt.Errorf("Unix device entry is missing the required \"path\" property.") } + } else if m["type"] == "usb" { + if m["productid"] == "" { + return fmt.Errorf("Missing productid for USB device.") + } } else if m["type"] == "none" { continue } else { diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go index ed85235..d6a5a6e 100644 --- a/lxd/container_lxc.go +++ b/lxd/container_lxc.go @@ -1080,6 +1080,8 @@ func (c *containerLXC) startCommon() (string, error) { c.removeUnixDevices() c.removeDiskDevices() + var usbs []usbDevice + // Create the devices for k, m := range c.expandedDevices { if shared.StringInSlice(m["type"], []string{"unix-char", "unix-block"}) { @@ -1099,6 +1101,45 @@ func (c *containerLXC) startCommon() (string, error) { if err != nil { return "", fmt.Errorf("Failed to add cgroup rule for device") } + } else if m["type"] == "usb" { + if usbs == nil { + usbs, err = deviceLoadUsb() + if err != nil { + return "", err + } + } + + for _, usb := range usbs { + if usb.vendor != m["vendorid"] || (m["productid"] != "" && usb.product != m["productid"]) { + continue + } + + err = lxcSetConfigItem(c.c, "lxc.cgroup.devices.allow", fmt.Sprintf("c %d:%d rwm", usb.major, usb.minor)) + if err != nil { + return "", err + } + + m["major"] = fmt.Sprintf("%d", usb.major) + m["minor"] = fmt.Sprintf("%d", usb.minor) + m["path"] = usb.path + + /* it's ok to fail, the device might be hot plugged later */ + _, err := c.createUnixDevice("unused", m) + if err != nil { + shared.Log.Warn("failed to create usb device", log.Ctx{"err": err, "device": k}) + continue + } + + /* if the create was successful, let's bind mount it */ + srcPath := usb.path + tgtPath := strings.TrimPrefix(srcPath, "/") + devName := fmt.Sprintf("unix.%s", strings.Replace(tgtPath, "/", "-", -1)) + devPath := filepath.Join(c.DevicesPath(), devName) + err = lxcSetConfigItem(c.c, "lxc.mount.entry", fmt.Sprintf("%s %s none bind,create=file", devPath, tgtPath)) + if err != nil { + return "", err + } + } } else if m["type"] == "disk" { // Disk device if m["path"] != "/" { @@ -2422,6 +2463,8 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error { } } + var usbs []usbDevice + // Live update the devices for k, m := range removeDevices { if shared.StringInSlice(m["type"], []string{"unix-char", "unix-block"}) { @@ -2439,6 +2482,29 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error { if err != nil { return err } + } else if m["type"] == "usb" { + if usbs == nil { + usbs, err = deviceLoadUsb() + if err != nil { + return err + } + } + + /* if the device isn't present, we don't need to remove it */ + for _, usb := range usbs { + if usb.vendor != m["vendorid"] || (m["productid"] != "" && usb.product != m["productid"]) { + continue + } + + m["major"] = fmt.Sprintf("%d", usb.major) + m["minor"] = fmt.Sprintf("%d", usb.minor) + m["path"] = usb.path + + err = c.removeUnixDevice(k, m) + if err != nil { + shared.Log.Error("failed to remove usb device", log.Ctx{"err": err, "usb": usb, "container": c.Name()}) + } + } } } @@ -2458,6 +2524,28 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error { if err != nil { return err } + } else if m["type"] == "usb" { + if usbs == nil { + usbs, err = deviceLoadUsb() + if err != nil { + return err + } + } + + for _, usb := range usbs { + if usb.vendor != m["vendorid"] || (m["productid"] != "" && usb.product != m["productid"]) { + continue + } + + m["major"] = fmt.Sprintf("%d", usb.major) + m["minor"] = fmt.Sprintf("%d", usb.minor) + m["path"] = usb.path + + err = c.insertUnixDevice(k, m) + if err != nil { + shared.Log.Error("failed to insert usb device", log.Ctx{"err": err, "usb": usb, "container": c.Name()}) + } + } } } diff --git a/lxd/db_devices.go b/lxd/db_devices.go index 6a8eea2..ae5a132 100644 --- a/lxd/db_devices.go +++ b/lxd/db_devices.go @@ -21,6 +21,8 @@ func dbDeviceTypeToString(t int) (string, error) { return "unix-char", nil case 4: return "unix-block", nil + case 5: + return "usb", nil default: return "", fmt.Errorf("Invalid device type %d", t) } @@ -38,6 +40,8 @@ func dbDeviceTypeToInt(t string) (int, error) { return 3, nil case "unix-block": return 4, nil + case "usb": + return 5, nil default: return -1, fmt.Errorf("Invalid device type %s", t) } diff --git a/lxd/devices.go b/lxd/devices.go index 529450d..3264452 100644 --- a/lxd/devices.go +++ b/lxd/devices.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "encoding/hex" "fmt" + "io/ioutil" "math/big" "os" "os/exec" @@ -43,7 +44,51 @@ func (c deviceTaskCPUs) Len() int { return len(c) } func (c deviceTaskCPUs) Less(i, j int) bool { return *c[i].count < *c[j].count } func (c deviceTaskCPUs) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -func deviceNetlinkListener() (chan []string, chan []string, error) { +type usbDevice struct { + action string + + vendor string + product string + + path string + major int + minor int +} + +func createUSBDevice(action string, vendor string, product string, major string, minor string, busnum string, devnum string) (usbDevice, error) { + majorInt, err := strconv.Atoi(minor) + if err != nil { + return usbDevice{}, err + } + + minorInt, err := strconv.Atoi(major) + if err != nil { + return usbDevice{}, err + } + + busnumInt, err := strconv.Atoi(busnum) + if err != nil { + return usbDevice{}, err + } + + devnumInt, err := strconv.Atoi(devnum) + if err != nil { + return usbDevice{}, err + } + + path := fmt.Sprintf("/dev/bus/usb/%03d/%03d", busnumInt, devnumInt) + + return usbDevice{ + action, + vendor, + product, + path, + majorInt, + minorInt, + }, nil +} + +func deviceNetlinkListener() (chan []string, chan []string, chan usbDevice, error) { NETLINK_KOBJECT_UEVENT := 15 UEVENT_BUFFER_SIZE := 2048 @@ -53,7 +98,7 @@ func deviceNetlinkListener() (chan []string, chan []string, error) { ) if err != nil { - return nil, nil, err + return nil, nil, nil, err } nl := syscall.SockaddrNetlink{ @@ -64,13 +109,14 @@ func deviceNetlinkListener() (chan []string, chan []string, error) { err = syscall.Bind(fd, &nl) if err != nil { - return nil, nil, err + return nil, nil, nil, err } chCPU := make(chan []string, 1) chNetwork := make(chan []string, 0) + chUSB := make(chan usbDevice) - go func(chCPU chan []string, chNetwork chan []string) { + go func(chCPU chan []string, chNetwork chan []string, chUSB chan usbDevice) { b := make([]byte, UEVENT_BUFFER_SIZE*2) for { _, err := syscall.Read(fd, b) @@ -126,10 +172,63 @@ func deviceNetlinkListener() (chan []string, chan []string, error) { // Network balancing is interface specific, so queue everything chNetwork <- []string{props["INTERFACE"], props["ACTION"]} } + + if props["SUBSYSTEM"] == "usb" { + if props["ACTION"] != "add" && props["ACTION"] != "remove" { + continue + } + + parts := strings.Split(props["PRODUCT"], "/") + if len(parts) < 2 { + continue + } + + major, ok := props["MAJOR"] + if !ok { + continue + } + minor, ok := props["MINOR"] + if !ok { + continue + } + busnum, ok := props["BUSNUM"] + if !ok { + continue + } + devnum, ok := props["DEVNUM"] + if !ok { + continue + } + + zeroPad := func(s string, l int) string { + return strings.Repeat("0", l - len(s)) + s + } + + usb, err := createUSBDevice( + props["ACTION"], + /* udev doesn't zero pad these, while + * everything else does, so let's zero pad them + * for consistency + */ + zeroPad(parts[0], 4), + zeroPad(parts[1], 4), + major, + minor, + busnum, + devnum, + ) + if err != nil { + shared.Log.Error("error reading usb device", log.Ctx{"err": err, "path": props["PHYSDEVPATH"]}) + continue + } + + chUSB <- usb + } + } - }(chCPU, chNetwork) + }(chCPU, chNetwork, chUSB) - return chCPU, chNetwork, nil + return chCPU, chNetwork, chUSB, nil } func parseCpuset(cpu string) ([]int, error) { @@ -354,8 +453,63 @@ func deviceNetworkPriority(d *Daemon, netif string) { return } +func deviceUSBEvent(d *Daemon, usb usbDevice) { + containers, err := dbContainersList(d.db, cTypeRegular) + if err != nil { + shared.Log.Error("problem loading containers list", log.Ctx{"err": err}) + return + } + for _, name := range containers { + containerIf, err := containerLoadByName(d, name) + if err != nil { + continue + } + + c, ok := containerIf.(*containerLXC) + if !ok { + shared.Log.Error("got device event on non-LXC container?") + return + } + + if !c.IsRunning() { + continue + } + + for _, m := range c.ExpandedDevices() { + if m["type"] != "usb" { + continue + } + + if m["vendorid"] != usb.vendor || (m["productid"] != "" && m["productid"] != usb.product) { + continue + } + + m["major"] = fmt.Sprintf("%d", usb.major) + m["minor"] = fmt.Sprintf("%d", usb.minor) + m["path"] = usb.path + + if usb.action == "add" { + err := c.insertUnixDevice("unused", m) + if err != nil { + shared.Log.Error("failed to create usb device", log.Ctx{"err": err, "usb": usb, "container": c.Name()}) + return + } + } else if usb.action == "remove" { + err := c.removeUnixDevice("unused", m) + if err != nil { + shared.Log.Error("failed to remove usb device", log.Ctx{"err": err, "usb": usb, "container": c.Name()}) + return + } + } else { + shared.Log.Error("unknown action for usb device", log.Ctx{"usb": usb}) + continue + } + } + } +} + func deviceEventListener(d *Daemon) { - chNetlinkCPU, chNetlinkNetwork, err := deviceNetlinkListener() + chNetlinkCPU, chNetlinkNetwork, chUSB, err := deviceNetlinkListener() if err != nil { shared.Log.Error("scheduler: couldn't setup netlink listener") return @@ -387,6 +541,8 @@ func deviceEventListener(d *Daemon) { shared.Debugf("Scheduler: network: %s has been added: updating network priorities", e[0]) deviceNetworkPriority(d, e[0]) + case e := <-chUSB: + deviceUSBEvent(d, e) case e := <-deviceSchedRebalance: if len(e) != 3 { shared.Log.Error("Scheduler: received an invalid rebalance event") @@ -819,3 +975,71 @@ func deviceParseDiskLimit(readSpeed string, writeSpeed string) (int64, int64, in return readBps, readIops, writeBps, writeIops, nil } + +const USB_PATH = "/sys/bus/usb/devices" + +func loadRawValues(p string) (map[string]string, error) { + values := map[string]string{ + "idVendor": "", + "idProduct": "", + "dev": "", + "busnum": "", + "devnum": "", + } + + for k, _ := range values { + v, err := ioutil.ReadFile(path.Join(p, k)) + if err != nil { + return nil, err + } + + values[k] = strings.TrimSpace(string(v)) + } + + return values, nil +} + +func deviceLoadUsb() ([]usbDevice, error) { + result := []usbDevice{} + + ents, err := ioutil.ReadDir(USB_PATH) + if err != nil { + return nil, err + } + + for _, ent := range ents { + values, err := loadRawValues(path.Join(USB_PATH, ent.Name())) + if err != nil { + if os.IsNotExist(err) { + continue + } + + return []usbDevice{}, err + } + + parts := strings.Split(values["dev"], ":") + if len(parts) != 2 { + return []usbDevice{}, fmt.Errorf("invalid device value %s", values["dev"]) + } + + usb, err := createUSBDevice( + "add", + values["idVendor"], + values["idProduct"], + parts[0], + parts[1], + values["busnum"], + values["devnum"], + ) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, err + } + + result = append(result, usb) + } + + return result, nil +}
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel