Repository: docker/docker-py

Issue:
ER: Support "device" for mapping host devices to containers 
We have some special host device files we need to access from inside the containers, it doesn't look as if the docker-py library permits passing --device?



Discussion:
You're looking to use the `devices` param in `docker.utils.create_host_config`. Unfortunately it looks like it's missing from the documentation, but the feature does exist already.

Ok thanks!

Documentation updated in 5d95f24264d25991e9f7caf23e9c860bcdb11874

I think 5d95f24 specifies how the `Devices` key looks in the resulting host config dict, not what the param should look like. Inside `create_host_config` the param is passed to `parse_devices` (https://github.com/docker/docker-py/blob/master/docker/utils/utils.py#L369), which expects it to be a list of strings like you would use in `docker run --device=...`. 

Correct - although, we should support both forms IMO, but it doesn't look like we do at the moment.


Resolving patch:
diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py
--- a/docker/utils/__init__.py
+++ b/docker/utils/__init__.py
@@ -4,7 +4,7 @@
     kwargs_from_env, convert_filters, datetime_to_timestamp, create_host_config,
     create_container_config, parse_bytes, ping_registry, parse_env_file,
     version_lt, version_gte, decode_json_header, split_command,
-    create_ipam_config, create_ipam_pool,
+    create_ipam_config, create_ipam_pool, parse_devices
 ) # flake8: noqa
 
 from .types import Ulimit, LogConfig # flake8: noqa
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -400,7 +400,7 @@ def parse_host(addr, platform=None):
             port = int(port)
         except Exception:
             raise errors.DockerException(
-                "Invalid port: %s", addr
+                "Invalid port: {0}".format(addr)
             )
 
     elif proto in ("http", "https") and ':' not in addr:
@@ -417,7 +417,14 @@ def parse_host(addr, platform=None):
 def parse_devices(devices):
     device_list = []
     for device in devices:
-        device_mapping = device.split(":")
+        if isinstance(device, dict):
+            device_list.append(device)
+            continue
+        if not isinstance(device, six.string_types):
+            raise errors.DockerException(
+                'Invalid device type {0}'.format(type(device))
+            )
+        device_mapping = device.split(':')
         if device_mapping:
             path_on_host = device_mapping[0]
             if len(device_mapping) > 1:
@@ -428,9 +435,11 @@ def parse_devices(devices):
                 permissions = device_mapping[2]
             else:
                 permissions = 'rwm'
-            device_list.append({"PathOnHost": path_on_host,
-                                "PathInContainer": path_in_container,
-                                "CgroupPermissions": permissions})
+            device_list.append({
+                'PathOnHost': path_on_host,
+                'PathInContainer': path_in_container,
+                'CgroupPermissions': permissions
+            })
     return device_list
 
 


Test patch:
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
--- a/tests/unit/utils_test.py
+++ b/tests/unit/utils_test.py
@@ -18,7 +18,7 @@
     parse_repository_tag, parse_host, convert_filters, kwargs_from_env,
     create_host_config, Ulimit, LogConfig, parse_bytes, parse_env_file,
     exclude_paths, convert_volume_binds, decode_json_header, tar,
-    split_command, create_ipam_config, create_ipam_pool,
+    split_command, create_ipam_config, create_ipam_pool, parse_devices,
 )
 from docker.utils.utils import create_endpoint_config
 from docker.utils.ports import build_port_bindings, split_port
@@ -406,6 +406,65 @@ def test_private_reg_image_sha(self):
         )
 
 
+class ParseDeviceTest(base.BaseTestCase):
+    def test_dict(self):
+        devices = parse_devices([{
+            'PathOnHost': '/dev/sda1',
+            'PathInContainer': '/dev/mnt1',
+            'CgroupPermissions': 'r'
+        }])
+        self.assertEqual(devices[0], {
+            'PathOnHost': '/dev/sda1',
+            'PathInContainer': '/dev/mnt1',
+            'CgroupPermissions': 'r'
+        })
+
+    def test_partial_string_definition(self):
+        devices = parse_devices(['/dev/sda1'])
+        self.assertEqual(devices[0], {
+            'PathOnHost': '/dev/sda1',
+            'PathInContainer': '/dev/sda1',
+            'CgroupPermissions': 'rwm'
+        })
+
+    def test_permissionless_string_definition(self):
+        devices = parse_devices(['/dev/sda1:/dev/mnt1'])
+        self.assertEqual(devices[0], {
+            'PathOnHost': '/dev/sda1',
+            'PathInContainer': '/dev/mnt1',
+            'CgroupPermissions': 'rwm'
+        })
+
+    def test_full_string_definition(self):
+        devices = parse_devices(['/dev/sda1:/dev/mnt1:r'])
+        self.assertEqual(devices[0], {
+            'PathOnHost': '/dev/sda1',
+            'PathInContainer': '/dev/mnt1',
+            'CgroupPermissions': 'r'
+        })
+
+    def test_hybrid_list(self):
+        devices = parse_devices([
+            '/dev/sda1:/dev/mnt1:rw',
+            {
+                'PathOnHost': '/dev/sda2',
+                'PathInContainer': '/dev/mnt2',
+                'CgroupPermissions': 'r'
+            }
+        ])
+
+        self.assertEqual(devices[0], {
+            'PathOnHost': '/dev/sda1',
+            'PathInContainer': '/dev/mnt1',
+            'CgroupPermissions': 'rw'
+        })
+        self.assertEqual(devices[1], {
+            'PathOnHost': '/dev/sda2',
+            'PathInContainer': '/dev/mnt2',
+            'CgroupPermissions': 'r'
+        })
+
+
 class UtilsTest(base.BaseTestCase):
     longMessage = True