[lxc-devel] [lxd/master] initial implementation of the "usb" device type
tych0 on Github
lxc-bot at linuxcontainers.org
Wed Aug 3 17:21:11 UTC 2016
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 646 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160803/4420d794/attachment.bin>
-------------- next part --------------
From e9a4c9f610fac2f11d5b34fade2d42a256bec3fc Mon Sep 17 00:00:00 2001
From: Tycho Andersen <tycho.andersen at 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.andersen at 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
+}
More information about the lxc-devel
mailing list