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

Reply via email to