Add --device flag to allow additional host devices in container

We add a --device flag which can be used like:

 docker run --device /dev/sda:/dev/xvda:rwm ubuntu /bin/bash

To allow the container to have read write permissions to access the host's /dev/sda via a node named /dev/xvda in the container.

Note: Much of this code was written by Dinesh Subhraveti dineshs@altiscale.com (github: dineshs-altiscale) and so he deserves a ton of credit.

Docker-DCO-1.1-Signed-off-by: Timothy <timothyhobbs@seznam.cz> (github: timthelion)
This commit is contained in:
Timothy 2014-05-31 04:00:47 +00:00 committed by Michael Crosby
parent 840ed5ace2
commit e855c4b921
5 changed files with 98 additions and 2 deletions

View file

@ -209,6 +209,20 @@ func populateCommand(c *Container, env []string) error {
return fmt.Errorf("invalid network mode: %s", c.hostConfig.NetworkMode)
}
// Build lists of devices allowed and created within the container.
userSpecifiedDevices := make([]*devices.Device, len(c.hostConfig.Devices))
for i, deviceMapping := range c.hostConfig.Devices {
device, err := devices.GetDevice(deviceMapping.PathOnHost, deviceMapping.CgroupPermissions)
device.Path = deviceMapping.PathInContainer
if err != nil {
return fmt.Errorf("error gathering device information while adding custom device %s", err)
}
userSpecifiedDevices[i] = device
}
allowedDevices := append(devices.DefaultAllowedDevices, userSpecifiedDevices...)
autoCreatedDevices := append(devices.DefaultAutoCreatedDevices, userSpecifiedDevices...)
// TODO: this can be removed after lxc-conf is fully deprecated
mergeLxcConfIntoOptions(c.hostConfig, context)
@ -231,8 +245,8 @@ func populateCommand(c *Container, env []string) error {
User: c.Config.User,
Config: context,
Resources: resources,
AllowedDevices: devices.DefaultAllowedDevices,
AutoCreatedDevices: devices.DefaultAutoCreatedDevices,
AllowedDevices: allowedDevices,
AutoCreatedDevices: autoCreatedDevices,
}
c.command.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
c.command.Env = env

View file

@ -946,6 +946,7 @@ removed before the image is removed.
-u, --user="" Username or UID
-v, --volume=[] Bind mount a volume (e.g., from the host: -v /host:/container, from docker: -v /container)
--volumes-from=[] Mount volumes from the specified container(s)
--device=[] Add a host device to the container (e.g. --device=/dev/sdc[:/dev/xvdc[:rwm]])
-w, --workdir="" Working directory inside the container
The `docker run` command first `creates` a writeable container layer over the
@ -1122,6 +1123,20 @@ logs could be retrieved using `docker logs`. This is
useful if you need to pipe a file or something else into a container and
retrieve the container's ID once the container has finished running.
$ sudo docker run --device=/dev/sdc:/dev/xvdc --device=/dev/sdd --device=/dev/zero:/dev/nulo -i -t ubuntu ls -l /dev/{xvdc,sdd,nulo}
brw-rw---- 1 root disk 8, 2 Feb 9 16:05 /dev/xvdc
brw-rw---- 1 root disk 8, 3 Feb 9 16:05 /dev/sdd
crw-rw-rw- 1 root root 1, 5 Feb 9 16:05 /dev/nulo
It is often necessary to directly expose devices to a container. ``--device``
option enables that. For example, a specific block storage device or loop
device or audio device can be added to an otherwise unprivileged container
(without the ``--privileged`` flag) and have the application directly access it.
** Security note: **
``--device`` cannot be safely used with ephemeral devices. Block devices that may be removed should not be added to untrusted containers with ``--device``!
**A complete example:**
$ sudo docker run -d --name static static-web-files sh

View file

@ -919,6 +919,22 @@ func TestRunUnprivilegedWithChroot(t *testing.T) {
logDone("run - unprivileged with chroot")
}
func TestAddingOptionalDevices(t *testing.T) {
cmd := exec.Command(dockerBinary, "run", "--device", "/dev/zero:/dev/nulo", "busybox", "sh", "-c", "ls /dev/nulo")
out, _, err := runCommandWithOutput(cmd)
if err != nil {
t.Fatal(err, out)
}
if actual := strings.Trim(out, "\r\n"); actual != "/dev/nulo" {
t.Fatalf("expected output /dev/nulo, received %s", actual)
}
deleteAllContainers()
logDone("run - test --device argument")
}
func TestModeHostname(t *testing.T) {
cmd := exec.Command(dockerBinary, "run", "-h=testhostname", "busybox", "cat", "/etc/hostname")

View file

@ -19,6 +19,12 @@ func (n NetworkMode) IsContainer() bool {
return len(parts) > 1 && parts[0] == "container"
}
type DeviceMapping struct {
PathOnHost string
PathInContainer string
CgroupPermissions string
}
type HostConfig struct {
Binds []string
ContainerIDFile string
@ -30,6 +36,7 @@ type HostConfig struct {
Dns []string
DnsSearch []string
VolumesFrom []string
Devices []DeviceMapping
NetworkMode NetworkMode
}
@ -42,6 +49,7 @@ func ContainerHostConfigFromJob(job *engine.Job) *HostConfig {
}
job.GetenvJson("LxcConf", &hostConfig.LxcConf)
job.GetenvJson("PortBindings", &hostConfig.PortBindings)
job.GetenvJson("Devices", &hostConfig.Devices)
if Binds := job.GetenvList("Binds"); Binds != nil {
hostConfig.Binds = Binds
}

View file

@ -41,6 +41,7 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf
flVolumes = opts.NewListOpts(opts.ValidatePath)
flLinks = opts.NewListOpts(opts.ValidateLink)
flEnv = opts.NewListOpts(opts.ValidateEnv)
flDevices = opts.NewListOpts(opts.ValidatePath)
flPublish opts.ListOpts
flExpose opts.ListOpts
@ -74,6 +75,7 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf
cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR.")
cmd.Var(&flVolumes, []string{"v", "-volume"}, "Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container)")
cmd.Var(&flLinks, []string{"#link", "-link"}, "Add link to another container in the form of name:alias")
cmd.Var(&flDevices, []string{"-device"}, "Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc)")
cmd.Var(&flEnv, []string{"e", "-env"}, "Set environment variables")
cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a line delimited file of environment variables")
@ -191,6 +193,16 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf
}
}
// parse device mappings
deviceMappings := []DeviceMapping{}
for _, device := range flDevices.GetAll() {
deviceMapping, err := ParseDevice(device)
if err != nil {
return nil, nil, cmd, err
}
deviceMappings = append(deviceMappings, deviceMapping)
}
// collect all the environment variables for the container
envVariables := []string{}
for _, ef := range flEnvFile.GetAll() {
@ -245,6 +257,7 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf
DnsSearch: flDnsSearch.GetAll(),
VolumesFrom: flVolumesFrom.GetAll(),
NetworkMode: netMode,
Devices: deviceMappings,
}
if sysInfo != nil && flMemory > 0 && !sysInfo.SwapLimit {
@ -303,3 +316,33 @@ func parseNetMode(netMode string) (NetworkMode, error) {
}
return NetworkMode(netMode), nil
}
func ParseDevice(device string) (DeviceMapping, error) {
src := ""
dst := ""
permissions := "rwm"
arr := strings.Split(device, ":")
switch len(arr) {
case 3:
permissions = arr[2]
fallthrough
case 2:
dst = arr[1]
fallthrough
case 1:
src = arr[0]
default:
return DeviceMapping{}, fmt.Errorf("Invalid device specification: %s", device)
}
if dst == "" {
dst = src
}
deviceMapping := DeviceMapping{
PathOnHost: src,
PathInContainer: dst,
CgroupPermissions: permissions,
}
return deviceMapping, nil
}