From 0a4153a76c85270ff2b92e5645ec993d2f81db54 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 18 Oct 2020 16:02:37 +0200 Subject: [PATCH 1/5] docker_container: add device_requests option. --- .../1119-docker_container-device-reqests.yml | 2 + .../modules/cloud/docker/docker_container.py | 83 +++++++++++++++++++ .../docker_container/tasks/tests/options.yml | 44 ++++++++++ 3 files changed, 129 insertions(+) create mode 100644 changelogs/fragments/1119-docker_container-device-reqests.yml diff --git a/changelogs/fragments/1119-docker_container-device-reqests.yml b/changelogs/fragments/1119-docker_container-device-reqests.yml new file mode 100644 index 00000000000..29347f1dfa5 --- /dev/null +++ b/changelogs/fragments/1119-docker_container-device-reqests.yml @@ -0,0 +1,2 @@ +minor_changes: +- "docker_container - now supports the ``device_requests`` option, which allows to request additional resources such as GPUs (https://github.com/ansible/ansible/issues/65748, https://github.com/ansible-collections/community.general/pull/1119)." diff --git a/plugins/modules/cloud/docker/docker_container.py b/plugins/modules/cloud/docker/docker_container.py index cfc893cdf74..4bc4c06186f 100644 --- a/plugins/modules/cloud/docker/docker_container.py +++ b/plugins/modules/cloud/docker/docker_container.py @@ -212,6 +212,42 @@ - "Must be a positive integer." type: int required: yes + device_requests: + description: + - Allows to request additional resources, such as GPUs. + type: list + elements: dict + suboptions: + capabilities: + description: + - List of lists of strings to request capabilities. + - The top-level list entries are combined by OR, and for every list entry, + the entries in the list it contains are combined by AND. + - The driver will try to satisfy one of the sub-lists. + - Available capabilities for the C(nvidia) driver can be found at + U(https://github.com/NVIDIA/nvidia-container-runtime). + type: list + elements: list + count: + description: + - Number or devices to request. + - Set to C(-1) to request all available devices. + type: int + device_ids: + description: + - List of device IDs. + type: list + elements: str + driver: + description: + - Which driver to use for this device. + type: str + options: + description: + - Driver-specific options. + type: dict +)), + dns_opts: description: - List of DNS options. @@ -1230,6 +1266,7 @@ def __init__(self, client): self.device_write_bps = None self.device_read_iops = None self.device_write_iops = None + self.device_requests = None self.dns_servers = None self.dns_opts = None self.dns_search_domains = None @@ -1391,6 +1428,22 @@ def __init__(self, client): if client.module.params.get(param_name): self._process_rate_iops(option=param_name) + if self.device_requests: + for dr_index, dr in enumerate(self.device_requests): + # Make sure that capabilities are lists of lists of strings + if dr['capabilities']: + for or_index, or_list in enumerate(dr['capabilities']): + for and_index, and_list in enumerate(or_list): + for term_index, term in enumerate(and_list): + if not isinstance(term, string_types): + self.fail( + "device_requests[{0}].capabilities[{1}][{2}][{3}] is not a string".format( + dr_index, or_index, and_index, term_index)) + and_list[term_index] = to_native(term) + # Make sure that options is a dictionary mapping strings to strings + if dr['options']: + dr['options'] = clean_dict_booleans_for_docker_api(dr['options']) + def fail(self, msg): self.client.fail(msg) @@ -1594,6 +1647,9 @@ def _host_config(self): if 'mounts' in params: params['mounts'] = self.mounts_opt + if self.device_requests is not None: + params['device_requests'] = [dict((k, v) for k, v in dr.items() if v is not None) for dr in self.device_requests] + return self.client.create_host_config(**params) @property @@ -1990,6 +2046,7 @@ def __init__(self, container, parameters): self.parameters.expected_sysctls = None self.parameters.expected_etc_hosts = None self.parameters.expected_env = None + self.parameters.expected_device_requests = None self.parameters_map = dict() self.parameters_map['expected_links'] = 'links' self.parameters_map['expected_ports'] = 'expected_ports' @@ -2005,6 +2062,7 @@ def __init__(self, container, parameters): self.parameters_map['expected_devices'] = 'devices' self.parameters_map['expected_healthcheck'] = 'healthcheck' self.parameters_map['expected_mounts'] = 'mounts' + self.parameters_map['expected_device_requests'] = 'device_requests' def fail(self, msg): self.parameters.client.fail(msg) @@ -2078,6 +2136,7 @@ def has_different_configuration(self, image): self.parameters.expected_cmd = self._get_expected_cmd() self.parameters.expected_devices = self._get_expected_devices() self.parameters.expected_healthcheck = self._get_expected_healthcheck() + self.parameters.expected_device_requests = self._get_expected_device_requests() if not self.container.get('HostConfig'): self.fail("has_config_diff: Error parsing container properties. HostConfig missing.") @@ -2155,6 +2214,7 @@ def has_different_configuration(self, image): device_write_bps=host_config.get('BlkioDeviceWriteBps'), device_read_iops=host_config.get('BlkioDeviceReadIOps'), device_write_iops=host_config.get('BlkioDeviceWriteIOps'), + expected_device_requests=host_config.get('DeviceRequests'), pids_limit=host_config.get('PidsLimit'), # According to https://github.com/moby/moby/, support for HostConfig.Mounts # has been included at least since v17.03.0-ce, which has API version 1.26. @@ -2454,6 +2514,20 @@ def _get_expected_binds(self, image): self.log(result, pretty_print=True) return result + def _get_expected_device_requests(self): + if self.parameters.device_requests is None: + return None + device_requests = [] + for dr in self.parameters.device_requests: + device_requests.append({ + 'Driver': dr['driver'], + 'Count': dr['count'], + 'DeviceIDs': dr['device_ids'], + 'Capabilities': dr['capabilities'], + 'Options': dr['options'], + }) + return device_requests + def _get_image_binds(self, volumes): ''' Convert array of binds to array of strings with format host_path:container_path:mode @@ -3089,6 +3163,7 @@ def _parse_comparisons(self): explicit_types = dict( command='list', devices='set(dict)', + device_requests='set(dict)', dns_search_domains='list', dns_servers='list', env='set', @@ -3222,6 +3297,7 @@ def __init__(self, **kwargs): device_read_iops=dict(docker_py_version='1.9.0', docker_api_version='1.22'), device_write_bps=dict(docker_py_version='1.9.0', docker_api_version='1.22'), device_write_iops=dict(docker_py_version='1.9.0', docker_api_version='1.22'), + device_requests=dict(docker_py_version='4.3.0', docker_api_version='1.40'), dns_opts=dict(docker_api_version='1.21', docker_py_version='1.10.0'), ipc_mode=dict(docker_api_version='1.25'), mac_address=dict(docker_api_version='1.25'), @@ -3320,6 +3396,13 @@ def main(): path=dict(required=True, type='str'), rate=dict(required=True, type='int'), )), + device_requests=dict(type='list', elements='dict', options=dict( + capabilities=dict(type='list', elements='list'), + count=dict(type='int'), + device_ids=dict(type='list', elements='str'), + driver=dict(type='str'), + options=dict(type='dict'), + )), dns_servers=dict(type='list', elements='str'), dns_opts=dict(type='list', elements='str'), dns_search_domains=dict(type='list', elements='str'), diff --git a/tests/integration/targets/docker_container/tasks/tests/options.yml b/tests/integration/targets/docker_container/tasks/tests/options.yml index 9958f08fa3d..2a156f22f27 100644 --- a/tests/integration/targets/docker_container/tasks/tests/options.yml +++ b/tests/integration/targets/docker_container/tasks/tests/options.yml @@ -891,6 +891,50 @@ - "'Minimum version required is 1.9.0 ' in device_write_limit_1.msg" when: docker_py_version is version('1.9.0', '<') +#################################################################### +## device_requests ################################################# +#################################################################### + +- name: device_requests + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_requests: [] + register: device_requests_1 + ignore_errors: yes + +- name: device_requests (idempotency) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_requests: [] + register: device_requests_2 + ignore_errors: yes + +- name: cleanup + docker_container: + name: "{{ cname }}" + state: absent + force_kill: yes + diff: no + +- assert: + that: + - device_requests_1 is changed + - device_requests_2 is not changed + when: docker_py_version is version('4.3.0', '>=') and docker_api_version is version('1.40', '>=') +- assert: + that: + - device_requests_1 is failed + - | + (('version is ' ~ docker_py_version ~ ' ') in device_requests_1.msg and 'Minimum version required is 4.3.0 ' in device_requests_1.msg) or + (('version is ' ~ docker_api_version ~ ' ') in device_requests_1.msg and 'Minimum version required is 1.40 ' in device_requests_1.msg) + when: docker_py_version is version('4.3.0', '<') or docker_api_version is version('1.40', '<') + #################################################################### ## dns_opts ######################################################## #################################################################### From a45cbec4461dbac151d78a661ddc841a9b088ab0 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 18 Oct 2020 16:10:13 +0200 Subject: [PATCH 2/5] Fix copy'n'paste mistake. --- plugins/modules/cloud/docker/docker_container.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/modules/cloud/docker/docker_container.py b/plugins/modules/cloud/docker/docker_container.py index 4bc4c06186f..5075d184da5 100644 --- a/plugins/modules/cloud/docker/docker_container.py +++ b/plugins/modules/cloud/docker/docker_container.py @@ -246,8 +246,6 @@ description: - Driver-specific options. type: dict -)), - dns_opts: description: - List of DNS options. From 62d1dea5c0184669a8ffa7a4792890b2d48a6fe6 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 18 Oct 2020 18:08:33 +0200 Subject: [PATCH 3/5] Fix failure test. --- .../targets/docker_container/tasks/tests/options.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/docker_container/tasks/tests/options.yml b/tests/integration/targets/docker_container/tasks/tests/options.yml index 2a156f22f27..2da58c6a7c3 100644 --- a/tests/integration/targets/docker_container/tasks/tests/options.yml +++ b/tests/integration/targets/docker_container/tasks/tests/options.yml @@ -932,7 +932,7 @@ - device_requests_1 is failed - | (('version is ' ~ docker_py_version ~ ' ') in device_requests_1.msg and 'Minimum version required is 4.3.0 ' in device_requests_1.msg) or - (('version is ' ~ docker_api_version ~ ' ') in device_requests_1.msg and 'Minimum version required is 1.40 ' in device_requests_1.msg) + (('API version is ' ~ docker_api_version ~ '.') in device_requests_1.msg and 'Minimum version required is 1.40 ' in device_requests_1.msg) when: docker_py_version is version('4.3.0', '<') or docker_api_version is version('1.40', '<') #################################################################### From 17a478f7eed8f77dd0c6690fe44dad3ae136b2e9 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 20 Oct 2020 07:18:23 +0200 Subject: [PATCH 4/5] Added example. --- .../modules/cloud/docker/docker_container.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plugins/modules/cloud/docker/docker_container.py b/plugins/modules/cloud/docker/docker_container.py index 5075d184da5..344d62f93c8 100644 --- a/plugins/modules/cloud/docker/docker_container.py +++ b/plugins/modules/cloud/docker/docker_container.py @@ -1081,6 +1081,26 @@ # Limit read rate for /dev/sdb to 300 IO per second - path: /dev/sdb rate: 300 + +- name: Start container with GPUs + community.general.docker_container: + name: test + image: ubuntu:18.04 + state: started + device_requests: + - # Add some specific devices to this container + device_ids: + - '0' + - 'GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2a' + - # Add nVidia GPUs to this container + driver: nvidia + count: -1 # this means we want all + capabilities: + # We have one OR condition: 'gpu' AND 'utility' + - - gpu + - utility + # See https://github.com/NVIDIA/nvidia-container-runtime#supported-driver-capabilities + # for a list of capabilities supported by the nvidia driver ''' RETURN = ''' From 36b2645db46ecc19adde875b1f2d85dd2b1995a9 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 20 Oct 2020 07:24:57 +0200 Subject: [PATCH 5/5] Adjust tense. --- plugins/modules/cloud/docker/docker_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/cloud/docker/docker_container.py b/plugins/modules/cloud/docker/docker_container.py index 344d62f93c8..2640b27af87 100644 --- a/plugins/modules/cloud/docker/docker_container.py +++ b/plugins/modules/cloud/docker/docker_container.py @@ -223,7 +223,7 @@ - List of lists of strings to request capabilities. - The top-level list entries are combined by OR, and for every list entry, the entries in the list it contains are combined by AND. - - The driver will try to satisfy one of the sub-lists. + - The driver tries to satisfy one of the sub-lists. - Available capabilities for the C(nvidia) driver can be found at U(https://github.com/NVIDIA/nvidia-container-runtime). type: list