[lxc-devel] [lxd/master] Uevent-based matching for `unix-char` and `unix-block`

splett2 on Github lxc-bot at linuxcontainers.org
Fri Nov 29 17:31:37 UTC 2019


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 343 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20191129/0f467cea/attachment-0001.bin>
-------------- next part --------------
From ba35c3c2169151d6c3d36cc5bc94d7edf7e5fa75 Mon Sep 17 00:00:00 2001
From: David Mao <david.mao.92 at gmail.com>
Date: Sat, 16 Nov 2019 17:43:47 -0600
Subject: [PATCH 1/5] lxd/device: Add unix_hotplug device type

Signed-off-by: Lillian Jan Johnson <lillianjanjohnson at gmail.com>
Signed-off-by: David Mao <david.mao at utexas.edu>
---
 lxd/device/device_utils_unix.go               |  21 ++
 .../device_utils_unix_hotplug_events.go       | 119 ++++++++++
 lxd/device/unix_hotplug.go                    | 212 ++++++++++++++++++
 3 files changed, 352 insertions(+)
 create mode 100644 lxd/device/device_utils_unix_hotplug_events.go
 create mode 100644 lxd/device/unix_hotplug.go

diff --git a/lxd/device/device_utils_unix.go b/lxd/device/device_utils_unix.go
index 2576d80152..0fe80873a7 100644
--- a/lxd/device/device_utils_unix.go
+++ b/lxd/device/device_utils_unix.go
@@ -378,6 +378,27 @@ func unixDeviceSetupCharNum(s *state.State, devicesPath string, typePrefix strin
 	return unixDeviceSetup(s, devicesPath, typePrefix, deviceName, configCopy, defaultMode, runConf)
 }
 
+// unixDeviceSetupBlockNum calls unixDeviceSetup and overrides the supplied device config with the
+// type as "unix-block" and the supplied major and minor numbers. This function can be used when you
+// already know the device's major and minor numbers to avoid unixDeviceSetup() having to stat the
+// device to ascertain these attributes. If defaultMode is true or mode is supplied in the device
+// config then the origin device does not need to be accessed for its file mode.
+func unixDeviceSetupBlockNum(s *state.State, devicesPath string, typePrefix string, deviceName string, m deviceConfig.Device, major uint32, minor uint32, path string, defaultMode bool, runConf *deviceConfig.RunConfig) error {
+	configCopy := deviceConfig.Device{}
+	for k, v := range m {
+		configCopy[k] = v
+	}
+
+	// Overridng these in the config copy should avoid the need for unixDeviceSetup to stat
+	// the origin device to ascertain this information.
+	configCopy["type"] = "unix-block"
+	configCopy["major"] = fmt.Sprintf("%d", major)
+	configCopy["minor"] = fmt.Sprintf("%d", minor)
+	configCopy["path"] = path
+
+	return unixDeviceSetup(s, devicesPath, typePrefix, deviceName, configCopy, defaultMode, runConf)
+}
+
 // UnixDeviceExists checks if the unix device already exists in devices path.
 func UnixDeviceExists(devicesPath string, prefix string, path string) bool {
 	relativeDestPath := strings.TrimPrefix(path, "/")
diff --git a/lxd/device/device_utils_unix_hotplug_events.go b/lxd/device/device_utils_unix_hotplug_events.go
new file mode 100644
index 0000000000..20683723eb
--- /dev/null
+++ b/lxd/device/device_utils_unix_hotplug_events.go
@@ -0,0 +1,119 @@
+package device
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"sync"
+
+	deviceConfig "github.com/lxc/lxd/lxd/device/config"
+	"github.com/lxc/lxd/lxd/state"
+	log "github.com/lxc/lxd/shared/log15"
+	"github.com/lxc/lxd/shared/logger"
+)
+
+// UnixHotplugEvent represents the properties of a Unix hotplug device uevent.
+type UnixHotplugEvent struct {
+	Action string
+
+	Vendor  string
+	Product string
+
+	Path        string
+	Major       uint32
+	Minor       uint32
+	Subsystem   string
+	UeventParts []string
+	UeventLen   int
+}
+
+// unixHotplugHandlers stores the event handler callbacks for Unix hotplug events.
+var unixHotplugHandlers = map[string]func(UnixHotplugEvent) (*deviceConfig.RunConfig, error){}
+
+// unixHotplugMutex controls access to the unixHotplugHandlers map.
+var unixHotplugMutex sync.Mutex
+
+// unixHotplugRegisterHandler registers a handler function to be called whenever a Unix hotplug device event occurs.
+func unixHotplugRegisterHandler(instance Instance, deviceName string, handler func(UnixHotplugEvent) (*deviceConfig.RunConfig, error)) {
+	unixHotplugMutex.Lock()
+	defer unixHotplugMutex.Unlock()
+
+	// Null delimited string of project name, instance name and device name.
+	key := fmt.Sprintf("%s\000%s\000%s", instance.Project(), instance.Name(), deviceName)
+	unixHotplugHandlers[key] = handler
+}
+
+// unixHotplugUnregisterHandler removes a registered Unix hotplug handler function for a device.
+func unixHotplugUnregisterHandler(instance Instance, deviceName string) {
+	unixHotplugMutex.Lock()
+	defer unixHotplugMutex.Unlock()
+
+	// Null delimited string of project name, instance name and device name.
+	key := fmt.Sprintf("%s\000%s\000%s", instance.Project(), instance.Name(), deviceName)
+	delete(unixHotplugHandlers, key)
+}
+
+// unixHotplugRunHandlers executes any handlers registered for Unix hotplug events.
+func UnixHotplugRunHandlers(state *state.State, event *UnixHotplugEvent) {
+	unixHotplugMutex.Lock()
+	defer unixHotplugMutex.Unlock()
+
+	for key, hook := range unixHotplugHandlers {
+		keyParts := strings.SplitN(key, "\000", 3)
+		projectName := keyParts[0]
+		instanceName := keyParts[1]
+		deviceName := keyParts[2]
+
+		if hook == nil {
+			delete(unixHotplugHandlers, key)
+			continue
+		}
+
+		runConf, err := hook(*event)
+		if err != nil {
+			logger.Error("Unix hotplug event hook failed", log.Ctx{"err": err, "project": projectName, "instance": instanceName, "device": deviceName})
+			continue
+		}
+
+		// If runConf supplied, load instance and call its Unix hotplug event handler function so
+		// any instance specific device actions can occur.
+		if runConf != nil {
+			instance, err := InstanceLoadByProjectAndName(state, projectName, instanceName)
+			if err != nil {
+				logger.Error("Unix hotplug event loading instance failed", log.Ctx{"err": err, "project": projectName, "instance": instanceName, "device": deviceName})
+				continue
+			}
+
+			err = instance.DeviceEventHandler(runConf)
+			if err != nil {
+				logger.Error("Unix hotplug event instance handler failed", log.Ctx{"err": err, "project": projectName, "instance": instanceName, "device": deviceName})
+				continue
+			}
+		}
+	}
+}
+
+// UnixHotplugNewEvent instantiates a new UnixHotplugEvent struct.
+func UnixHotplugNewEvent(action string, vendor string, product string, major string, minor string, subsystem string, devname string, ueventParts []string, ueventLen int) (UnixHotplugEvent, error) {
+	majorInt, err := strconv.ParseUint(major, 10, 32)
+	if err != nil {
+		return UnixHotplugEvent{}, err
+	}
+
+	minorInt, err := strconv.ParseUint(minor, 10, 32)
+	if err != nil {
+		return UnixHotplugEvent{}, err
+	}
+
+	return UnixHotplugEvent{
+		action,
+		vendor,
+		product,
+		devname,
+		uint32(majorInt),
+		uint32(minorInt),
+		subsystem,
+		ueventParts,
+		ueventLen,
+	}, nil
+}
diff --git a/lxd/device/unix_hotplug.go b/lxd/device/unix_hotplug.go
new file mode 100644
index 0000000000..94bc378318
--- /dev/null
+++ b/lxd/device/unix_hotplug.go
@@ -0,0 +1,212 @@
+package device
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+   
+    deviceConfig "github.com/lxc/lxd/lxd/device/config"
+	"github.com/farjump/go-libudev"
+	"github.com/lxc/lxd/lxd/instance/instancetype"
+	"github.com/lxc/lxd/shared"
+)
+
+// unixHotplugIsOurDevice indicates whether the unixHotplug device event qualifies as part of our device.
+// This function is not defined against the unixHotplug struct type so that it can be used in event
+// callbacks without needing to keep a reference to the unixHotplug device struct.
+func unixHotplugIsOurDevice(config deviceConfig.Device, unixHotplug *UnixHotplugEvent) bool {
+	// Check if event matches criteria for this device, if not return.
+	if (config["vendorid"] != "" && config["vendorid"] != unixHotplug.Vendor) || (config["productid"] != "" && config["productid"] != unixHotplug.Product) {
+		return false
+	}
+
+	return true
+}
+
+type unixHotplug struct {
+	deviceCommon
+}
+
+// isRequired indicates whether the device config requires this device to start OK.
+func (d *unixHotplug) isRequired() bool {
+	// Defaults to not required.
+	if shared.IsTrue(d.config["required"]) {
+		return true
+	}
+
+	return false
+}
+
+// validateConfig checks the supplied config for correctness.
+func (d *unixHotplug) validateConfig() error {
+	if d.instance.Type() != instancetype.Container {
+		return ErrUnsupportedDevType
+	}
+
+	rules := map[string]func(string) error{
+		"vendorid":  shared.IsDeviceID,
+		"productid": shared.IsDeviceID,
+		"uid":       unixValidUserID,
+		"gid":       unixValidUserID,
+		"mode":      unixValidOctalFileMode,
+		"required":  shared.IsBool,
+	}
+
+	err := d.config.Validate(rules)
+	if err != nil {
+		return err
+	}
+
+	if d.config["vendorid"] == "" && d.config["productid"] == "" {
+		return fmt.Errorf("Unix hotplug devices require a vendorid or a productid")
+	}
+
+	return nil
+}
+
+// Register is run after the device is started or when LXD starts.
+func (d *unixHotplug) Register() error {
+	// Extract variables needed to run the event hook so that the reference to this device
+	// struct is not needed to be kept in memory.
+	devicesPath := d.instance.DevicesPath()
+	devConfig := d.config
+	deviceName := d.name
+	state := d.state
+
+	// Handler for when a UnixHotplug event occurs.
+	f := func(e UnixHotplugEvent) (*deviceConfig.RunConfig, error) {
+		if !unixHotplugIsOurDevice(devConfig, &e) {
+			return nil, nil
+		}
+
+		runConf := deviceConfig.RunConfig{}
+
+		if e.Action == "add" {
+			if e.Subsystem == "char" {
+				err := unixDeviceSetupCharNum(state, devicesPath, "unix", deviceName, devConfig, e.Major, e.Minor, e.Path, false, &runConf)
+				if err != nil {
+					return nil, err
+				}
+			} else {
+				err := unixDeviceSetupBlockNum(state, devicesPath, "unix", deviceName, devConfig, e.Major, e.Minor, e.Path, false, &runConf)
+				if err != nil {
+					return nil, err
+				}
+			}
+		} else if e.Action == "remove" {
+			relativeTargetPath := strings.TrimPrefix(e.Path, "/")
+			err := unixDeviceRemove(devicesPath, "unix", deviceName, relativeTargetPath, &runConf)
+			if err != nil {
+				return nil, err
+			}
+
+			// Add a post hook function to remove the specific unix hotplug device file after unmount.
+			runConf.PostHooks = []func() error{func() error {
+				err := unixDeviceDeleteFiles(state, devicesPath, "unix", deviceName, relativeTargetPath)
+				if err != nil {
+					return fmt.Errorf("Failed to delete files for device '%s': %v", deviceName, err)
+				}
+
+				return nil
+			}}
+		}
+
+		runConf.Uevents = append(runConf.Uevents, e.UeventParts)
+
+		return &runConf, nil
+	}
+
+	unixHotplugRegisterHandler(d.instance, d.name, f)
+
+	return nil
+}
+
+// Start is run when the device is added to the instance
+func (d *unixHotplug) Start() (*deviceConfig.RunConfig, error) {
+	runConf := deviceConfig.RunConfig{}
+	runConf.PostHooks = []func() error{d.Register}
+
+	device := d.loadUnixDevice()
+	if d.isRequired() && device == nil {
+		return nil, fmt.Errorf("Required Unix Hotplug device not found")
+	}
+	if device == nil {
+		return &runConf, nil
+	}
+
+	i, err := strconv.ParseUint(device.PropertyValue("MAJOR"), 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	major := uint32(i)
+	j, err := strconv.ParseUint(device.PropertyValue("MINOR"), 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	minor := uint32(j)
+
+	// setup device
+	if device.Subsystem() == "char" {
+		err = unixDeviceSetupCharNum(d.state, d.instance.DevicesPath(), "unix", d.name, d.config, major, minor, device.Devnode(), false, &runConf)
+	} else if device.Subsystem() == "block" {
+		err = unixDeviceSetupBlockNum(d.state, d.instance.DevicesPath(), "unix", d.name, d.config, major, minor, device.Devnode(), false, &runConf)
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &runConf, nil
+}
+
+// Stop is run when the device is removed from the instance
+func (d *unixHotplug) Stop() (*deviceConfig.RunConfig, error) {
+	unixHotplugUnregisterHandler(d.instance, d.name)
+
+	runConf := deviceConfig.RunConfig{
+		PostHooks: []func() error{d.postStop},
+	}
+
+	err := unixDeviceRemove(d.instance.DevicesPath(), "unix", d.name, "", &runConf)
+	if err != nil {
+		return nil, err
+	}
+
+	return &runConf, nil
+}
+
+// postStop is run after the device is removed from the instance
+func (d *unixHotplug) postStop() error {
+	err := unixDeviceDeleteFiles(d.state, d.instance.DevicesPath(), "unix", d.name, "")
+	if err != nil {
+		return fmt.Errorf("Failed to delete files for device '%s': %v", d.name, err)
+	}
+
+	return nil
+}
+
+// loadUnixDevice scans the host machine for unix devices with matching product/vendor ids
+// and returns the first matching device with the subsystem type char or block
+func (d *unixHotplug) loadUnixDevice() *udev.Device {
+	// Find device if exists
+	u := udev.Udev{}
+	e := u.NewEnumerate()
+
+	if d.config["vendorid"] != "" {
+		e.AddMatchProperty("ID_VENDOR_ID", d.config["vendorid"])
+	}
+	if d.config["productid"] != "" {
+		e.AddMatchProperty("ID_MODEL_ID", d.config["productid"])
+	}
+	e.AddMatchIsInitialized()
+	devices, _ := e.Devices()
+	var device *udev.Device
+	for i := range devices {
+		device = devices[i]
+		if device.Subsystem() == "block" || device.Subsystem() == "char" {
+			return device
+		}
+	}
+
+	return nil
+}

From 77b8a0019cdc06cd3a894e6381acdafaf7c6f16d Mon Sep 17 00:00:00 2001
From: David Mao <david.mao.92 at gmail.com>
Date: Tue, 19 Nov 2019 18:05:39 -0600
Subject: [PATCH 2/5] lxd/device: Add support for listening to unix char and
 block udev events

Signed-off-by: David Mao <david.mao at utexas.edu>
Signed-off-by: Lillian Jan Johnson <lillianjanjohnson at gmail.com>
---
 lxd/device/device.go | 19 +++++-----
 lxd/devices.go       | 88 ++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 90 insertions(+), 17 deletions(-)

diff --git a/lxd/device/device.go b/lxd/device/device.go
index c608f80bc9..9745867d7c 100644
--- a/lxd/device/device.go
+++ b/lxd/device/device.go
@@ -9,15 +9,16 @@ import (
 
 // devTypes defines supported top-level device type creation functions.
 var devTypes = map[string]func(deviceConfig.Device) device{
-	"nic":        nicLoadByType,
-	"infiniband": infinibandLoadByType,
-	"proxy":      func(c deviceConfig.Device) device { return &proxy{} },
-	"gpu":        func(c deviceConfig.Device) device { return &gpu{} },
-	"usb":        func(c deviceConfig.Device) device { return &usb{} },
-	"unix-char":  func(c deviceConfig.Device) device { return &unixCommon{} },
-	"unix-block": func(c deviceConfig.Device) device { return &unixCommon{} },
-	"disk":       func(c deviceConfig.Device) device { return &disk{} },
-	"none":       func(c deviceConfig.Device) device { return &none{} },
+	"nic":          nicLoadByType,
+	"infiniband":   infinibandLoadByType,
+	"proxy":        func(c deviceConfig.Device) device { return &proxy{} },
+	"gpu":          func(c deviceConfig.Device) device { return &gpu{} },
+	"usb":          func(c deviceConfig.Device) device { return &usb{} },
+	"unix-char":    func(c deviceConfig.Device) device { return &unixCommon{} },
+	"unix-block":   func(c deviceConfig.Device) device { return &unixCommon{} },
+	"unix-hotplug": func(c deviceConfig.Device) device { return &unixHotplug{} },
+	"disk":         func(c deviceConfig.Device) device { return &disk{} },
+	"none":         func(c deviceConfig.Device) device { return &none{} },
 }
 
 // VolatileSetter is a function that accepts one or more key/value strings to save into the LXD
diff --git a/lxd/devices.go b/lxd/devices.go
index 5939a0c494..7a9cd3738b 100644
--- a/lxd/devices.go
+++ b/lxd/devices.go
@@ -34,7 +34,7 @@ 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, chan device.USBEvent, error) {
+func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent, chan device.UnixHotplugEvent, error) {
 	NETLINK_KOBJECT_UEVENT := 15
 	UEVENT_BUFFER_SIZE := 2048
 
@@ -43,25 +43,26 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 		NETLINK_KOBJECT_UEVENT,
 	)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, nil, nil, err
 	}
 
 	nl := unix.SockaddrNetlink{
 		Family: unix.AF_NETLINK,
 		Pid:    uint32(os.Getpid()),
-		Groups: 1,
+		Groups: 3,
 	}
 
 	err = unix.Bind(fd, &nl)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, nil, nil, err
 	}
 
 	chCPU := make(chan []string, 1)
 	chNetwork := make(chan []string, 0)
 	chUSB := make(chan device.USBEvent)
+	chUnix := make(chan device.UnixHotplugEvent)
 
-	go func(chCPU chan []string, chNetwork chan []string, chUSB chan device.USBEvent) {
+	go func(chCPU chan []string, chNetwork chan []string, chUSB chan device.USBEvent, chUnix chan device.UnixHotplugEvent) {
 		b := make([]byte, UEVENT_BUFFER_SIZE*2)
 		for {
 			r, err := unix.Read(fd, b)
@@ -72,6 +73,7 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 			ueventBuf := make([]byte, r)
 			copy(ueventBuf, b)
 			ueventLen := 0
+			udevEvent := false
 			ueventParts := strings.Split(string(ueventBuf), "\x00")
 			props := map[string]string{}
 			for _, part := range ueventParts {
@@ -79,6 +81,10 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 					continue
 				}
 
+				if strings.HasPrefix(part, "libudev") {
+					udevEvent = true
+				}
+
 				ueventLen += len(part) + 1
 
 				fields := strings.SplitN(part, "=", 2)
@@ -180,10 +186,74 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 				chUSB <- usb
 			}
 
+			if (props["SUBSYSTEM"] == "char" || props["SUBSYSTEM"] == "block") && udevEvent {
+				vendor, ok := props["ID_VENDOR_ID"]
+				if !ok {
+					continue
+				}
+
+				product, ok := props["ID_MODEL_ID"]
+				if !ok {
+					continue
+				}
+
+				major, ok := props["MAJOR"]
+				if !ok {
+					continue
+				}
+
+				minor, ok := props["MINOR"]
+				if !ok {
+					continue
+				}
+
+				devname, ok := props["DEVNAME"]
+				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
+				}
+
+				unix, err := device.UnixHotplugNewEvent(
+					props["ACTION"],
+					/* udev doesn't zero pad these, while
+					 * everything else does, so let's zero pad them
+					 * for consistency
+					 */
+					zeroPad(vendor, 4),
+					zeroPad(product, 4),
+					major,
+					minor,
+					busnum,
+					devnum,
+					devname,
+					ueventParts[:len(ueventParts)-1],
+					ueventLen,
+				)
+				if err != nil {
+					logger.Error("Error reading unix device", log.Ctx{"err": err, "path": props["PHYSDEVPATH"]})
+					continue
+				}
+
+				chUnix <- unix
+			}
+
 		}
-	}(chCPU, chNetwork, chUSB)
+	}(chCPU, chNetwork, chUSB, chUnix)
 
-	return chCPU, chNetwork, chUSB, nil
+	return chCPU, chNetwork, chUSB, chUnix, nil
 }
 
 func parseCpuset(cpu string) ([]int, error) {
@@ -440,7 +510,7 @@ func deviceNetworkPriority(s *state.State, netif string) {
 }
 
 func deviceEventListener(s *state.State) {
-	chNetlinkCPU, chNetlinkNetwork, chUSB, err := deviceNetlinkListener()
+	chNetlinkCPU, chNetlinkNetwork, chUSB, chUnix, err := deviceNetlinkListener()
 	if err != nil {
 		logger.Errorf("scheduler: Couldn't setup netlink listener: %v", err)
 		return
@@ -475,6 +545,8 @@ func deviceEventListener(s *state.State) {
 			networkAutoAttach(s.Cluster, e[0])
 		case e := <-chUSB:
 			device.USBRunHandlers(s, &e)
+		case e := <-chUnix:
+			device.UnixHotplugRunHandlers(s, &e)
 		case e := <-cgroup.DeviceSchedRebalance:
 			if len(e) != 3 {
 				logger.Errorf("Scheduler: received an invalid rebalance event")

From b817720285fad8e128f1e90407d978876409cf91 Mon Sep 17 00:00:00 2001
From: David Mao <david.mao.92 at gmail.com>
Date: Wed, 27 Nov 2019 10:12:26 -0600
Subject: [PATCH 3/5] lxd/db: added device type Signed-off-by: Lillian J.
 Johnson <lillianjanjohnson at gmail.com> Signed-off-by: David Mao
 <david.mao at utexas.edu>

---
 lxd/db/devices.go |  4 ++++
 lxd/devices.go    | 25 ++++++++++---------------
 2 files changed, 14 insertions(+), 15 deletions(-)

diff --git a/lxd/db/devices.go b/lxd/db/devices.go
index 3691fd3478..e4fca429ed 100644
--- a/lxd/db/devices.go
+++ b/lxd/db/devices.go
@@ -29,6 +29,8 @@ func dbDeviceTypeToString(t int) (string, error) {
 		return "infiniband", nil
 	case 8:
 		return "proxy", nil
+	case 9:
+		return "unix-hotplug", nil
 	default:
 		return "", fmt.Errorf("Invalid device type %d", t)
 	}
@@ -54,6 +56,8 @@ func dbDeviceTypeToInt(t string) (int, error) {
 		return 7, nil
 	case "proxy":
 		return 8, nil
+	case "unix-hotplug":
+		return 9, nil
 	default:
 		return -1, fmt.Errorf("Invalid device type %s", t)
 	}
diff --git a/lxd/devices.go b/lxd/devices.go
index 7a9cd3738b..1fbe2e123d 100644
--- a/lxd/devices.go
+++ b/lxd/devices.go
@@ -83,6 +83,7 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 
 				if strings.HasPrefix(part, "libudev") {
 					udevEvent = true
+					continue
 				}
 
 				ueventLen += len(part) + 1
@@ -97,7 +98,7 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 
 			ueventLen--
 
-			if props["SUBSYSTEM"] == "cpu" {
+			if props["SUBSYSTEM"] == "cpu" && !udevEvent {
 				if props["DRIVER"] != "processor" {
 					continue
 				}
@@ -114,7 +115,7 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 				}
 			}
 
-			if props["SUBSYSTEM"] == "net" {
+			if props["SUBSYSTEM"] == "net" && !udevEvent {
 				if props["ACTION"] != "add" && props["ACTION"] != "removed" {
 					continue
 				}
@@ -127,7 +128,7 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 				chNetwork <- []string{props["INTERFACE"], props["ACTION"]}
 			}
 
-			if props["SUBSYSTEM"] == "usb" {
+			if props["SUBSYSTEM"] == "usb" && !udevEvent {
 				parts := strings.Split(props["PRODUCT"], "/")
 				if len(parts) < 2 {
 					continue
@@ -187,6 +188,11 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 			}
 
 			if (props["SUBSYSTEM"] == "char" || props["SUBSYSTEM"] == "block") && udevEvent {
+				subsystem, ok := props["SUBSYSTEM"]
+				if !ok {
+					continue
+				}
+
 				vendor, ok := props["ID_VENDOR_ID"]
 				if !ok {
 					continue
@@ -212,16 +218,6 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 					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
 				}
@@ -236,8 +232,7 @@ func deviceNetlinkListener() (chan []string, chan []string, chan device.USBEvent
 					zeroPad(product, 4),
 					major,
 					minor,
-					busnum,
-					devnum,
+					subsystem,
 					devname,
 					ueventParts[:len(ueventParts)-1],
 					ueventLen,

From 486e500177dfc88dafadce5a1e835c27e20996cc Mon Sep 17 00:00:00 2001
From: Lily <Lily>
Date: Fri, 29 Nov 2019 10:10:37 -0600
Subject: [PATCH 4/5] api: Add extention for new device type unix hotplug
 Signed-off-by: David Mao <david.mao at utexas.edu> Signed-off-by: Lillian Jan
 Johnson <lillianjanjohnson at gmail.com>

---
 doc/api-extensions.md | 3 +++
 shared/version/api.go | 1 +
 2 files changed, 4 insertions(+)

diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index 1b836a0623..980dddb2ac 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -886,3 +886,6 @@ This allows for existing a CEPH RDB or FS to be directly connected to a LXD cont
 
 ## virtual\_machines
 Add virtual machine support.
+
+## unix\_hotplug\_devices
+Adds support for unix char and block device hotplugging.
diff --git a/shared/version/api.go b/shared/version/api.go
index 1afdc1b2d0..1f36ee5c4f 100644
--- a/shared/version/api.go
+++ b/shared/version/api.go
@@ -179,6 +179,7 @@ var APIExtensions = []string{
 	"container_syscall_intercept_mount_fuse",
 	"container_disk_ceph",
 	"virtual-machines",
+	"unix_hotplug_devices",
 }
 
 // APIExtensionsCount returns the number of available API extensions.

From a382a1c8c33a0abb7d325aa17aed508d99d540d6 Mon Sep 17 00:00:00 2001
From: Lily <Lily>
Date: Fri, 29 Nov 2019 10:15:00 -0600
Subject: [PATCH 5/5] doc/instances: added new device type unix hotplug
 Signed-off-by: David Mao <david.mao at utexas.edu> Signed-off-by: Lillian Jan
 Johnson <lillianjanjohnson at gmail.com>

---
 doc/instances.md | 39 ++++++++++++++++++++++++++++-----------
 1 file changed, 28 insertions(+), 11 deletions(-)

diff --git a/doc/instances.md b/doc/instances.md
index 9ef017d60a..da4ae31c4b 100644
--- a/doc/instances.md
+++ b/doc/instances.md
@@ -221,17 +221,18 @@ lxc profile device add <profile> <name> <type> [key=value]...
 ## Device types
 LXD supports the following device types:
 
-ID (database)   | Name                              | Condition     | Description
-:--             | :--                               | :--           | :--
-0               | [none](#type-none)                | -             | Inheritance blocker
-1               | [nic](#type-nic)                  | -             | Network interface
-2               | [disk](#type-disk)                | -             | Mountpoint inside the instance
-3               | [unix-char](#type-unix-char)      | container     | Unix character device
-4               | [unix-block](#type-unix-block)    | container     | Unix block device
-5               | [usb](#type-usb)                  | container     | USB device
-6               | [gpu](#type-gpu)                  | container     | GPU device
-7               | [infiniband](#type-infiniband)    | container     | Infiniband device
-8               | [proxy](#type-proxy)              | container     | Proxy device
+ID (database)   | Name                               | Condition     | Description
+:--             | :--                                | :--           | :--
+0               | [none](#type-none)                 | -             | Inheritance blocker
+1               | [nic](#type-nic)                   | -             | Network interface
+2               | [disk](#type-disk)                 | -             | Mountpoint inside the instance
+3               | [unix-char](#type-unix-char)       | container     | Unix character device
+4               | [unix-block](#type-unix-block)     | container     | Unix block device
+5               | [usb](#type-usb)                   | container     | USB device
+6               | [gpu](#type-gpu)                   | container     | GPU device
+7               | [infiniband](#type-infiniband)     | container     | Infiniband device
+8               | [proxy](#type-proxy)               | container     | Proxy device
+9               | [unix-hotplug](#type-unix-hotplug) | container     | Unix hotplug device
 
 ### Type: none
 A none type device doesn't have any property and doesn't create anything inside the instance.
@@ -666,6 +667,22 @@ proxy\_protocol | bool      | false         | no        | Whether to use the HAP
 security.uid    | int       | 0             | no        | What UID to drop privilege to
 security.gid    | int       | 0             | no        | What GID to drop privilege to
 
+### Type: unix-hotplug
+Unix hotplug device entries make the requested unix device
+appear in the container's `/dev` and allow read/write operations to it
+if the device exists on the host system.
+
+The following properties exist:
+
+Key         | Type      | Default           | Required  | Description
+:--         | :--       | :--               | :--       | :--
+vendorid    | string    | -                 | no        | The vendor id of the USB device.
+productid   | string    | -                 | no        | The product 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
+required    | boolean   | false             | no        | Whether or not this device is required to start the container. (The default is false, and all devices are hot-pluggable)
+
 ```
 lxc config device add <instance> <device-name> proxy listen=<type>:<addr>:<port>[-<port>][,<port>] connect=<type>:<addr>:<port> bind=<host/instance>
 ```


More information about the lxc-devel mailing list