[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