[lxc-devel] [lxd/master] add gpu device type

brauner on Github lxc-bot at linuxcontainers.org
Thu Oct 20 10:35:08 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 1781 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20161020/cc9c05b9/attachment.bin>
-------------- next part --------------
From 1809909a600fa8a73ab52c40148beef3fe9ac0b5 Mon Sep 17 00:00:00 2001
From: Christian Brauner <christian.brauner at canonical.com>
Date: Tue, 18 Oct 2016 11:17:27 +0200
Subject: [PATCH 1/2] devices: add gpu device type

- lxc config device add c1 gpu0 gpu => Pass everything we have
- lxc config device add c1 gpu0 gpu vendor=10de => Pass whatever we have from nvidia
- lxc config device add c1 gpu0 gpu vendor=10de id=1 => Pass the second nvidia GPU
- lxc config device add c1 gpu0 gpu pci=0000:02:08.0 => Pass whatever GPU is at that PCI address

This parses /sys/bus/pci/devices/* and looks for the drm subfolder. All
subfolders under that, e.g.

	card0
	controlD64
	renderD128

belong to the same PCI address and probably all need to be handed to the
container when the user somehow selects card0.
We then retrieve the vendor ID, and if available the device ID from

	/sys/bus/pci/devices/*/vendor
	/sys/bus/pci/devices/*/device

The major and minor number for each device can be gathered from the

	card0/dev
	controlD64/dev
	renderD128/dev

files.

If we detect that the vendor ID is Nvidia, we also parse /dev and look for

	/dev/nvidia[0-9]+

entries. We thereby associate the correct /dev/nvidia* entry with the correct
/dev/card* entry, e.g. we correlate

	/dev/card0
and
	/dev/nvidia0.

Finally, we store any residual /dev/nvidia[^0-9]+ mounts that are not cards
because we need to assume that if /dev/nvidia[0-9]+ is requested then the other
files are necessary for correct functionality of that card and need to be
mounted into every container that also mounts that card.

Signed-off-by: Christian Brauner <christian.brauner at canonical.com>
---
 lxd/container.go     |  22 ++++-
 lxd/container_lxc.go | 231 +++++++++++++++++++++++++++++++++++++++++++++++----
 lxd/db_devices.go    |   4 +
 lxd/devices.go       | 211 +++++++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 449 insertions(+), 19 deletions(-)

diff --git a/lxd/container.go b/lxd/container.go
index dbc19fe..5b8ddcb 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -152,6 +152,23 @@ func containerValidDeviceConfigKey(t, k string) bool {
 		default:
 			return false
 		}
+	case "gpu":
+		switch k {
+		case "vendorid":
+			return true
+		case "id":
+			return true
+		case "mode":
+			return true
+		case "gid":
+			return true
+		case "uid":
+			return true
+		case "pci":
+			return true
+		default:
+			return false
+		}
 	case "none":
 		return false
 	default:
@@ -204,7 +221,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", "usb"}) {
+		if !shared.StringInSlice(m["type"], []string{"none", "nic", "disk", "unix-char", "unix-block", "usb", "gpu"}) {
 			return fmt.Errorf("Invalid device type for device '%s'", name)
 		}
 
@@ -254,6 +271,9 @@ func containerValidDevices(devices shared.Devices, profile bool, expanded bool)
 			if m["vendorid"] == "" {
 				return fmt.Errorf("Missing vendorid for USB device.")
 			}
+		} else if m["type"] == "gpu" {
+			// Probably no checks needed, since we allow users to
+			// pass in all GPUs.
 		} else if m["type"] == "none" {
 			continue
 		} else {
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 7170548..9696d72 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -1283,6 +1283,135 @@ func (c *containerLXC) startCommon() (string, error) {
 			if !created && shared.IsTrue(m["required"]) {
 				return "", fmt.Errorf("couldn't create usb device %s", k)
 			}
+		} else if m["type"] == "gpu" {
+			gpus, nvidiaDevices, err := deviceLoadGpu()
+			if err != nil {
+				return "", err
+			}
+
+			sawNvidia := false
+			for _, gpu := range gpus {
+				if m["vendorid"] != "" && gpu.vendor != m["vendorid"] {
+					continue
+				}
+
+				if m["pci"] != "" && gpu.pci != m["pci"] {
+					continue
+				}
+
+				if m["id"] != "" && gpu.id != m["id"] {
+					continue
+				}
+
+				if c.IsPrivileged() && !runningInUserns && cgDevicesController {
+					err = lxcSetConfigItem(c.c, "lxc.cgroup.devices.allow", fmt.Sprintf("c %d:%d rwm", gpu.major, gpu.minor))
+					if err != nil {
+						return "", err
+					}
+				}
+
+				temp := shared.Device{}
+				if err := shared.DeepCopy(&m, &temp); err != nil {
+					return "", err
+				}
+
+				temp["major"] = fmt.Sprintf("%d", gpu.major)
+				temp["minor"] = fmt.Sprintf("%d", gpu.minor)
+				temp["path"] = gpu.path
+
+				/* it's ok to fail, the device might be hot plugged later */
+				_, err := c.createUnixDevice(temp)
+				if err != nil {
+					shared.LogDebug("failed to create gpu device", log.Ctx{"err": err, "device": k})
+					continue
+				}
+
+				/* if the create was successful, let's bind mount it */
+				srcPath := gpu.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
+				}
+
+				if gpu.nvidia.path == "" {
+					continue
+				}
+
+				if c.IsPrivileged() && !runningInUserns && cgDevicesController {
+					err = lxcSetConfigItem(c.c, "lxc.cgroup.devices.allow", fmt.Sprintf("c %d:%d rwm", gpu.nvidia.major, gpu.nvidia.minor))
+					if err != nil {
+						return "", err
+					}
+				}
+
+				temp = shared.Device{}
+				if err := shared.DeepCopy(&m, &temp); err != nil {
+					return "", err
+				}
+
+				temp["major"] = fmt.Sprintf("%d", gpu.nvidia.major)
+				temp["minor"] = fmt.Sprintf("%d", gpu.nvidia.minor)
+				temp["path"] = gpu.nvidia.path
+
+				/* it's ok to fail, the device might be hot plugged later */
+				_, err = c.createUnixDevice(temp)
+				if err != nil {
+					shared.LogDebug("failed to create gpu device", log.Ctx{"err": err, "device": k})
+					continue
+				}
+
+				sawNvidia = true
+
+				/* if the create was successful, let's bind mount it */
+				srcPath = gpu.nvidia.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
+				}
+			}
+
+			if sawNvidia {
+				for _, gpu := range nvidiaDevices {
+					if c.IsPrivileged() && !runningInUserns && cgDevicesController {
+						err = lxcSetConfigItem(c.c, "lxc.cgroup.devices.allow", fmt.Sprintf("c %d:%d rwm", gpu.major, gpu.minor))
+						if err != nil {
+							return "", err
+						}
+					}
+
+					temp := shared.Device{}
+					if err := shared.DeepCopy(&m, &temp); err != nil {
+						return "", err
+					}
+
+					temp["major"] = fmt.Sprintf("%d", gpu.major)
+					temp["minor"] = fmt.Sprintf("%d", gpu.minor)
+					temp["path"] = gpu.path
+
+					/* it's ok to fail, the device might be hot plugged later */
+					_, err := c.createUnixDevice(temp)
+					if err != nil {
+						shared.LogDebug("failed to create gpu device", log.Ctx{"err": err, "device": k})
+						continue
+					}
+
+					/* if the create was successful, let's bind mount it */
+					srcPath := gpu.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"] != "/" {
@@ -2793,7 +2922,45 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error {
 						continue
 					}
 
-					err := c.removeUSBDevice(m, usb)
+					err := c.removeUnixDeviceNum(m, usb.major, usb.minor, usb.path)
+					if err != nil {
+						return err
+					}
+				}
+			} else if m["type"] == "gpu" {
+				gpus, nvidiaDevices, err := deviceLoadGpu()
+				if err != nil {
+					return err
+				}
+
+				for _, gpu := range gpus {
+					if m["vendorid"] != "" && gpu.vendor != m["vendorid"] {
+						continue
+					}
+
+					if m["pci"] != "" && gpu.pci != m["pci"] {
+						continue
+					}
+
+					if m["id"] != "" && gpu.id != m["id"] {
+						continue
+					}
+
+					err := c.removeUnixDeviceNum(m, gpu.major, gpu.minor, gpu.path)
+					if err != nil {
+						return err
+					}
+					if gpu.nvidia.path != "" {
+						err := c.removeUnixDeviceNum(m, gpu.nvidia.major, gpu.nvidia.minor, gpu.nvidia.path)
+						if err != nil {
+							return err
+						}
+					}
+				}
+
+				/* if the device isn't present, we don't need to remove it */
+				for _, gpu := range nvidiaDevices {
+					err := c.removeUnixDeviceNum(m, gpu.major, gpu.minor, gpu.path)
 					if err != nil {
 						return err
 					}
@@ -2830,11 +2997,47 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error {
 						continue
 					}
 
-					err = c.insertUSBDevice(m, usb)
+					err = c.insertUnixDeviceNum(m, usb.major, usb.minor, usb.path)
 					if err != nil {
 						shared.LogError("failed to insert usb device", log.Ctx{"err": err, "usb": usb, "container": c.Name()})
 					}
 				}
+			} else if m["type"] == "gpu" {
+				gpus, nvidiaDevices, err := deviceLoadGpu()
+				if err != nil {
+					return err
+				}
+
+				for _, gpu := range gpus {
+					if m["vendorid"] != "" && gpu.vendor != m["vendorid"] {
+						continue
+					}
+
+					if m["pci"] != "" && gpu.pci != m["pci"] {
+						continue
+					}
+
+					if m["id"] != "" && gpu.id != m["id"] {
+						continue
+					}
+
+					err = c.insertUnixDeviceNum(m, gpu.major, gpu.minor, gpu.path)
+					if err != nil {
+						shared.LogError("failed to insert gpu device", log.Ctx{"err": err, "gpu": gpu, "container": c.Name()})
+					}
+					if gpu.nvidia.path != "" {
+						err = c.insertUnixDeviceNum(m, gpu.nvidia.major, gpu.nvidia.minor, gpu.nvidia.path)
+						if err != nil {
+							shared.LogError("failed to insert gpu device", log.Ctx{"err": err, "gpu": gpu, "container": c.Name()})
+						}
+					}
+				}
+				for _, gpu := range nvidiaDevices {
+					err = c.insertUnixDeviceNum(m, gpu.major, gpu.minor, gpu.path)
+					if err != nil {
+						shared.LogError("failed to insert gpu device", log.Ctx{"err": err, "gpu": gpu, "container": c.Name()})
+					}
+				}
 			}
 		}
 
@@ -4202,15 +4405,15 @@ func (c *containerLXC) insertUnixDevice(m shared.Device) error {
 	return nil
 }
 
-func (c *containerLXC) insertUSBDevice(m shared.Device, usb usbDevice) error {
+func (c *containerLXC) insertUnixDeviceNum(m shared.Device, major int, minor int, path string) error {
 	temp := shared.Device{}
 	if err := shared.DeepCopy(&m, &temp); err != nil {
 		return err
 	}
 
-	temp["major"] = fmt.Sprintf("%d", usb.major)
-	temp["minor"] = fmt.Sprintf("%d", usb.minor)
-	temp["path"] = usb.path
+	temp["major"] = fmt.Sprintf("%d", major)
+	temp["minor"] = fmt.Sprintf("%d", minor)
+	temp["path"] = path
 
 	return c.insertUnixDevice(temp)
 }
@@ -4269,7 +4472,7 @@ func (c *containerLXC) removeUnixDevice(m shared.Device) error {
 	return nil
 }
 
-func (c *containerLXC) removeUSBDevice(m shared.Device, usb usbDevice) error {
+func (c *containerLXC) removeUnixDeviceNum(m shared.Device, major int, minor int, path string) error {
 	pid := c.InitPID()
 	if pid == -1 {
 		return fmt.Errorf("Can't remove device from stopped container")
@@ -4280,21 +4483,17 @@ func (c *containerLXC) removeUSBDevice(m shared.Device, usb usbDevice) error {
 		return err
 	}
 
-	temp["major"] = fmt.Sprintf("%d", usb.major)
-	temp["minor"] = fmt.Sprintf("%d", usb.minor)
-	temp["path"] = usb.path
+	temp["major"] = fmt.Sprintf("%d", major)
+	temp["minor"] = fmt.Sprintf("%d", minor)
+	temp["path"] = path
 
 	err := c.removeUnixDevice(temp)
 	if err != nil {
-		shared.LogError("failed to remove usb device", log.Ctx{"err": err, "usb": usb, "container": c.Name()})
+		shared.LogError("failed to remove gpu device", log.Ctx{"err": err, "gpu": path, "container": c.Name()})
 		return err
 	}
 
-	/* ok to fail here, there may be other usb
-	 * devices on this bus still left in the
-	 * container
-	 */
-	dir := fmt.Sprintf("/proc/%d/root/%s", pid, filepath.Dir(usb.path))
+	dir := fmt.Sprintf("/proc/%d/root/%s", pid, filepath.Dir(path))
 	os.Remove(dir)
 	return nil
 }
diff --git a/lxd/db_devices.go b/lxd/db_devices.go
index ae5a132..c65afd8 100644
--- a/lxd/db_devices.go
+++ b/lxd/db_devices.go
@@ -23,6 +23,8 @@ func dbDeviceTypeToString(t int) (string, error) {
 		return "unix-block", nil
 	case 5:
 		return "usb", nil
+	case 6:
+		return "gpu", nil
 	default:
 		return "", fmt.Errorf("Invalid device type %d", t)
 	}
@@ -42,6 +44,8 @@ func dbDeviceTypeToInt(t string) (int, error) {
 		return 4, nil
 	case "usb":
 		return 5, nil
+	case "gpu":
+		return 6, nil
 	default:
 		return -1, fmt.Errorf("Invalid device type %s", t)
 	}
diff --git a/lxd/devices.go b/lxd/devices.go
index 6a30a0f..5e9e7bd 100644
--- a/lxd/devices.go
+++ b/lxd/devices.go
@@ -12,6 +12,7 @@ import (
 	"os/exec"
 	"path"
 	"path/filepath"
+	"regexp"
 	"sort"
 	"strconv"
 	"strings"
@@ -55,6 +56,212 @@ type usbDevice struct {
 	minor int
 }
 
+// /dev/nvidia[0-9]+
+type nvidiaGpuCards struct {
+	path  string
+	major int
+	minor int
+	id    string
+}
+
+// {/dev/nvidiactl, /dev/nvidia-uvm, ...}
+type nvidiaGpuDevices struct {
+	path  string
+	major int
+	minor int
+}
+
+// /dev/dri/card0. If we detect that vendor == nvidia, then nvidia will contain
+// the corresponding nvidia car, e.g. {/dev/dri/card1 --> /dev/nvidia1}.
+type gpuDevice struct {
+	nvidia nvidiaGpuCards
+	// If related devices have the same PCI address as the GPU we should
+	// mount them all. Meaning if we detect /dev/dri/card0,
+	// /dev/dri/controlD64, and /dev/dri/renderD128 with the same PCI
+	// address, then they should all be made available in the container.
+	pci  string
+	path string
+
+	major  int
+	minor  int
+	vendor string
+	device string
+	id     string
+}
+
+func isNvidiaGpu(vendorId string) bool {
+	return strings.EqualFold(vendorId, "10de")
+}
+
+type cardIds struct {
+	id  string
+	pci string
+}
+
+func deviceLoadGpu() ([]gpuDevice, []nvidiaGpuDevices, error) {
+	const DRI_PATH = "/sys/bus/pci/devices"
+	var gpus []gpuDevice
+	var nvidiaDevices []nvidiaGpuDevices
+	var cards []cardIds
+
+	ents, err := ioutil.ReadDir(DRI_PATH)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return gpus, nvidiaDevices, nil
+		}
+		return gpus, nvidiaDevices, err
+	}
+
+	isNvidia := false
+	for _, ent := range ents {
+		// The pci address == the name of the directory. So let's use
+		// this cheap way of retrieving it.
+		pciAddr := ent.Name()
+
+		// Make sure that we are dealing with a GPU by looking whether
+		// the "drm" subfolder exists.
+		drm := filepath.Join(DRI_PATH, pciAddr, "drm")
+		drmEnts, err := ioutil.ReadDir(drm)
+		if err != nil {
+			if os.IsNotExist(err) {
+				continue
+			}
+		}
+
+		// Retrieve vendor ID.
+		vendorIdPath := filepath.Join(DRI_PATH, pciAddr, "vendor")
+		vendorId, err := ioutil.ReadFile(vendorIdPath)
+		if err != nil {
+			if os.IsNotExist(err) {
+				continue
+			}
+		}
+
+		// Retrieve device ID.
+		deviceIdPath := filepath.Join(DRI_PATH, pciAddr, "device")
+		deviceId, err := ioutil.ReadFile(deviceIdPath)
+		if err != nil {
+			if os.IsNotExist(err) {
+				continue
+			}
+		}
+
+		// Store all associated subdevices, e.g. controlD64, renderD128.
+		// The name of the directory == the last part of the
+		// /dev/dri/controlD64 path. So ent.Name() will give us
+		// controlD64.
+		for _, drmEnt := range drmEnts {
+			vendorTmp := strings.TrimSpace(string(vendorId))
+			deviceTmp := strings.TrimSpace(string(deviceId))
+			vendorTmp = strings.TrimPrefix(vendorTmp, "0x")
+			deviceTmp = strings.TrimPrefix(deviceTmp, "0x")
+			tmpGpu := gpuDevice{
+				pci:    pciAddr,
+				vendor: vendorTmp,
+				device: deviceTmp,
+				path:   filepath.Join("/dev/dri", drmEnt.Name()),
+			}
+
+			majMinPath := filepath.Join(drm, drmEnt.Name(), "dev")
+			majMinByte, err := ioutil.ReadFile(majMinPath)
+			if err != nil {
+				if os.IsNotExist(err) {
+					continue
+				}
+			}
+			majMin := strings.TrimSpace(string(majMinByte))
+			majMinSlice := strings.Split(string(majMin), ":")
+			if len(majMinSlice) != 2 {
+				continue
+			}
+			majorInt, err := strconv.Atoi(majMinSlice[0])
+			if err != nil {
+				continue
+			}
+			minorInt, err := strconv.Atoi(majMinSlice[1])
+			if err != nil {
+				continue
+			}
+
+			tmpGpu.major = majorInt
+			tmpGpu.minor = minorInt
+
+			isCard, err := regexp.MatchString("^card", drmEnt.Name())
+			if isCard {
+				// If it is a card it's minor number will be its id.
+				tmpGpu.id = strconv.Itoa(minorInt)
+				tmp := cardIds{
+					id:  tmpGpu.id,
+					pci: tmpGpu.pci,
+				}
+				cards = append(cards, tmp)
+			}
+			// Find matching /dev/nvidia* entry for /dev/dri/card*
+			if isNvidiaGpu(tmpGpu.vendor) && isCard {
+				if !isNvidia {
+					isNvidia = true
+				}
+				nvidiaPath := "/dev/nvidia" + strconv.Itoa(tmpGpu.minor)
+				stat := syscall.Stat_t{}
+				err := syscall.Stat(nvidiaPath, &stat)
+				if err != nil {
+					continue
+				}
+				tmpGpu.nvidia.path = nvidiaPath
+				tmpGpu.nvidia.major = int(stat.Rdev / 256)
+				tmpGpu.nvidia.minor = int(stat.Rdev % 256)
+				tmpGpu.nvidia.id = strconv.Itoa(tmpGpu.nvidia.minor)
+			}
+			gpus = append(gpus, tmpGpu)
+		}
+	}
+
+	// We detected a Nvidia card, so let's collect all other nvidia devices
+	// that are not /dev/nvidia[0-9]+.
+	if isNvidia {
+		nvidiaEnts, err := ioutil.ReadDir("/dev")
+		if err != nil {
+			if os.IsNotExist(err) {
+				return []gpuDevice{}, []nvidiaGpuDevices{}, err
+			}
+		}
+		validNvidia, err := regexp.Compile(`^nvidia[^0-9]+`)
+		if err != nil {
+			return gpus, nil, nil
+		}
+		for _, nvidiaEnt := range nvidiaEnts {
+			if !validNvidia.MatchString(nvidiaEnt.Name()) {
+				continue
+			}
+			nvidiaPath := filepath.Join("/dev", nvidiaEnt.Name())
+			stat := syscall.Stat_t{}
+			err = syscall.Stat(nvidiaPath, &stat)
+			if err != nil {
+				continue
+			}
+			tmpNividiaGpu := nvidiaGpuDevices{
+				path:  nvidiaPath,
+				major: int(stat.Rdev / 256),
+				minor: int(stat.Rdev % 256),
+			}
+			nvidiaDevices = append(nvidiaDevices, tmpNividiaGpu)
+		}
+
+	}
+
+	// Since we'll give users to ability to specify and id we need to group
+	// devices on the same PCI that belong to the same card by id.
+	for _, card := range cards {
+		for _, gpu := range gpus {
+			if gpu.pci == card.pci {
+				gpu.id = card.id
+			}
+		}
+	}
+
+	return gpus, nvidiaDevices, nil
+}
+
 func createUSBDevice(action string, vendor string, product string, major string, minor string, busnum string, devnum string, devname string) (usbDevice, error) {
 	majorInt, err := strconv.Atoi(major)
 	if err != nil {
@@ -504,13 +711,13 @@ func deviceUSBEvent(d *Daemon, usb usbDevice) {
 			}
 
 			if usb.action == "add" {
-				err := c.insertUSBDevice(m, usb)
+				err := c.insertUnixDeviceNum(m, usb.major, usb.minor, usb.path)
 				if err != nil {
 					shared.LogError("failed to create usb device", log.Ctx{"err": err, "usb": usb, "container": c.Name()})
 					return
 				}
 			} else if usb.action == "remove" {
-				err := c.removeUSBDevice(m, usb)
+				err := c.removeUnixDeviceNum(m, usb.major, usb.minor, usb.path)
 				if err != nil {
 					shared.LogError("failed to remove usb device", log.Ctx{"err": err, "usb": usb, "container": c.Name()})
 					return

From 7ed3bd2e1364d9ce634737aff0cc976cdf181f52 Mon Sep 17 00:00:00 2001
From: Christian Brauner <christian.brauner at canonical.com>
Date: Thu, 20 Oct 2016 12:29:32 +0200
Subject: [PATCH 2/2] doc, api: add gpu device type

Signed-off-by: Christian Brauner <christian.brauner at canonical.com>
---
 doc/configuration.md | 14 ++++++++++++++
 lxd/api_1.0.go       |  1 +
 2 files changed, 15 insertions(+)

diff --git a/doc/configuration.md b/doc/configuration.md
index a5405ea..0eb5231 100644
--- a/doc/configuration.md
+++ b/doc/configuration.md
@@ -277,6 +277,20 @@ gid         | int       | 0                 | no        | GID of the device owne
 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 no, and all devices are hot-pluggable.)
 
+### Type: gpu
+GPU device entries simply make the requested gpu device appear in the
+container.
+
+The following properties exist:
+
+Key         | Type      | Default           | Required  | Description
+:--         | :--       | :--               | :--       | :--
+vendorid    | string    | -                 | no        | The vendor id of the GPU device.
+id          | string    | -                 | no        | The card id of the GPU 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 7638271..7e8f369 100644
--- a/lxd/api_1.0.go
+++ b/lxd/api_1.0.go
@@ -75,6 +75,7 @@ func api10Get(d *Daemon, r *http.Request) Response {
 			"container_exec_recording",
 			"certificate_update",
 			"container_exec_signal_handling",
+			"gpu_devices",
 		},
 
 		"api_status":  "stable",


More information about the lxc-devel mailing list