From 19a7aa462b977a9d5f1c68b0fe0ccea76529434b Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2020 22:43:25 +0100 Subject: [PATCH] docker_container: add device_requests option (#1119) (#1186) * docker_container: add device_requests option. * Fix copy'n'paste mistake. * Fix failure test. * Added example. * Adjust tense. (cherry picked from commit 8670eff750f084eebccba81949d66ae0a0315404) Co-authored-by: Felix Fontein --- .../1119-docker_container-device-reqests.yml | 2 + .../modules/cloud/docker/docker_container.py | 101 ++++++++++++++++++ .../docker_container/tasks/tests/options.yml | 44 ++++++++ 3 files changed, 147 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 0000000000..29347f1dfa --- /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 cfc893cdf7..2640b27af8 100644 --- a/plugins/modules/cloud/docker/docker_container.py +++ b/plugins/modules/cloud/docker/docker_container.py @@ -212,6 +212,40 @@ options: - "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 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 + 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. @@ -1047,6 +1081,26 @@ EXAMPLES = ''' # 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 = ''' @@ -1230,6 +1284,7 @@ class TaskParameters(DockerBaseClass): 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 +1446,22 @@ class TaskParameters(DockerBaseClass): 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 +1665,9 @@ class TaskParameters(DockerBaseClass): 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 +2064,7 @@ class Container(DockerBaseClass): 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 +2080,7 @@ class Container(DockerBaseClass): 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 +2154,7 @@ class Container(DockerBaseClass): 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 +2232,7 @@ class Container(DockerBaseClass): 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 +2532,20 @@ class Container(DockerBaseClass): 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 +3181,7 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): explicit_types = dict( command='list', devices='set(dict)', + device_requests='set(dict)', dns_search_domains='list', dns_servers='list', env='set', @@ -3222,6 +3315,7 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): 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 +3414,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 9958f08fa3..2da58c6a7c 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 + (('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', '<') + #################################################################### ## dns_opts ######################################################## ####################################################################