Merge pull request #1400 from DanElbert/754-device_option

Added devices config handling and device HostConfig handling
This commit is contained in:
Daniel Nephin 2015-05-11 12:42:48 -04:00
commit 4997facbb4
6 changed files with 85 additions and 38 deletions

View File

@ -10,6 +10,7 @@ DOCKER_CONFIG_KEYS = [
'cpuset',
'command',
'detach',
'devices',
'dns',
'dns_search',
'domainname',
@ -50,6 +51,7 @@ DOCKER_CONFIG_HINTS = {
'add_host': 'extra_hosts',
'hosts': 'extra_hosts',
'extra_host': 'extra_hosts',
'device': 'devices',
'link': 'links',
'port': 'ports',
'privilege': 'privileged',
@ -200,10 +202,13 @@ def merge_service_dicts(base, override):
override.get('environment'),
)
if 'volumes' in base or 'volumes' in override:
d['volumes'] = merge_volumes(
base.get('volumes'),
override.get('volumes'),
path_mapping_keys = ['volumes', 'devices']
for key in path_mapping_keys:
if key in base or key in override:
d[key] = merge_path_mappings(
base.get(key),
override.get(key),
)
if 'labels' in base or 'labels' in override:
@ -230,7 +235,7 @@ def merge_service_dicts(base, override):
if key in base or key in override:
d[key] = to_list(base.get(key)) + to_list(override.get(key))
already_merged_keys = ['environment', 'volumes', 'labels'] + list_keys + list_or_string_keys
already_merged_keys = ['environment', 'labels'] + path_mapping_keys + list_keys + list_or_string_keys
for k in set(ALLOWED_KEYS) - set(already_merged_keys):
if k in override:
@ -346,7 +351,7 @@ def resolve_host_paths(volumes, working_dir=None):
def resolve_host_path(volume, working_dir):
container_path, host_path = split_volume(volume)
container_path, host_path = split_path_mapping(volume)
if host_path is not None:
host_path = os.path.expanduser(host_path)
host_path = os.path.expandvars(host_path)
@ -368,24 +373,24 @@ def validate_paths(service_dict):
raise ConfigurationError("build path %s either does not exist or is not accessible." % build_path)
def merge_volumes(base, override):
d = dict_from_volumes(base)
d.update(dict_from_volumes(override))
return volumes_from_dict(d)
def merge_path_mappings(base, override):
d = dict_from_path_mappings(base)
d.update(dict_from_path_mappings(override))
return path_mappings_from_dict(d)
def dict_from_volumes(volumes):
if volumes:
return dict(split_volume(v) for v in volumes)
def dict_from_path_mappings(path_mappings):
if path_mappings:
return dict(split_path_mapping(v) for v in path_mappings)
else:
return {}
def volumes_from_dict(d):
return [join_volume(v) for v in d.items()]
def path_mappings_from_dict(d):
return [join_path_mapping(v) for v in d.items()]
def split_volume(string):
def split_path_mapping(string):
if ':' in string:
(host, container) = string.split(':', 1)
return (container, host)
@ -393,7 +398,7 @@ def split_volume(string):
return (string, None)
def join_volume(pair):
def join_path_mapping(pair):
(container, host) = pair
if host is None:
return container

View File

@ -20,6 +20,7 @@ log = logging.getLogger(__name__)
DOCKER_START_KEYS = [
'cap_add',
'cap_drop',
'devices',
'dns',
'dns_search',
'env_file',
@ -441,6 +442,8 @@ class Service(object):
extra_hosts = build_extra_hosts(options.get('extra_hosts', None))
read_only = options.get('read_only', None)
devices = options.get('devices', None)
return create_host_config(
links=self._get_links(link_to_self=one_off),
port_bindings=port_bindings,
@ -448,6 +451,7 @@ class Service(object):
volumes_from=options.get('volumes_from'),
privileged=privileged,
network_mode=self._get_net(),
devices=devices,
dns=dns,
dns_search=dns_search,
restart_policy=restart,

View File

@ -342,8 +342,8 @@ environment:
- BAZ=local
```
Finally, for `volumes`, Compose "merges" entries together with locally-defined
bindings taking precedence:
Finally, for `volumes` and `devices`, Compose "merges" entries together with
locally-defined bindings taking precedence:
```yaml
# original service

View File

@ -342,6 +342,16 @@ dns_search:
- dc2.example.com
```
### devices
List of device mappings. Uses the same format as the `--device` docker
client create option.
```
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"
```
### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty, cpu\_shares, cpuset, read\_only
Each of these is a single value, analogous to its

View File

@ -669,3 +669,16 @@ class ServiceTest(DockerClientTestCase):
self.assertEqual('none', log_config['Type'])
self.assertFalse(log_config['Config'])
def test_devices(self):
service = self.create_service('web', devices=["/dev/random:/dev/mapped-random"])
device_config = create_and_start_container(service).get('HostConfig.Devices')
device_dict = {
'PathOnHost': '/dev/random',
'CgroupPermissions': 'rwm',
'PathInContainer': '/dev/mapped-random'
}
self.assertEqual(1, len(device_config))
self.assertDictEqual(device_dict, device_config[0])

View File

@ -54,46 +54,61 @@ class VolumePathTest(unittest.TestCase):
self.assertEqual(d['volumes'], ['/home/user:/container/path'])
class MergeVolumesTest(unittest.TestCase):
class MergePathMappingTest(object):
def config_name(self):
return ""
def test_empty(self):
service_dict = config.merge_service_dicts({}, {})
self.assertNotIn('volumes', service_dict)
self.assertNotIn(self.config_name(), service_dict)
def test_no_override(self):
service_dict = config.merge_service_dicts(
{'volumes': ['/foo:/code', '/data']},
{self.config_name(): ['/foo:/code', '/data']},
{},
)
self.assertEqual(set(service_dict['volumes']), set(['/foo:/code', '/data']))
self.assertEqual(set(service_dict[self.config_name()]), set(['/foo:/code', '/data']))
def test_no_base(self):
service_dict = config.merge_service_dicts(
{},
{'volumes': ['/bar:/code']},
{self.config_name(): ['/bar:/code']},
)
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code']))
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code']))
def test_override_explicit_path(self):
service_dict = config.merge_service_dicts(
{'volumes': ['/foo:/code', '/data']},
{'volumes': ['/bar:/code']},
{self.config_name(): ['/foo:/code', '/data']},
{self.config_name(): ['/bar:/code']},
)
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/data']))
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code', '/data']))
def test_add_explicit_path(self):
service_dict = config.merge_service_dicts(
{'volumes': ['/foo:/code', '/data']},
{'volumes': ['/bar:/code', '/quux:/data']},
{self.config_name(): ['/foo:/code', '/data']},
{self.config_name(): ['/bar:/code', '/quux:/data']},
)
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/quux:/data']))
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code', '/quux:/data']))
def test_remove_explicit_path(self):
service_dict = config.merge_service_dicts(
{'volumes': ['/foo:/code', '/quux:/data']},
{'volumes': ['/bar:/code', '/data']},
{self.config_name(): ['/foo:/code', '/quux:/data']},
{self.config_name(): ['/bar:/code', '/data']},
)
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/data']))
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code', '/data']))
class MergeVolumesTest(unittest.TestCase, MergePathMappingTest):
def config_name(self):
return 'volumes'
class MergeDevicesTest(unittest.TestCase, MergePathMappingTest):
def config_name(self):
return 'devices'
class BuildOrImageMergeTest(unittest.TestCase):
def test_merge_build_or_image_no_override(self):
self.assertEqual(
config.merge_service_dicts({'build': '.'}, {}),