Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docker_container: add device_requests option #1119

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/1119-docker_container-device-reqests.yml
Original file line number Diff line number Diff line change
@@ -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)."
101 changes: 101 additions & 0 deletions plugins/modules/cloud/docker/docker_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,40 @@
- "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.
Expand Down Expand Up @@ -1047,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 = '''
Expand Down Expand Up @@ -1230,6 +1284,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
Expand Down Expand Up @@ -1391,6 +1446,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)

Expand Down Expand Up @@ -1594,6 +1665,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
Expand Down Expand Up @@ -1990,6 +2064,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'
Expand All @@ -2005,6 +2080,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)
Expand Down Expand Up @@ -2078,6 +2154,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.")
Expand Down Expand Up @@ -2155,6 +2232,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.
Expand Down Expand Up @@ -2454,6 +2532,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
Expand Down Expand Up @@ -3089,6 +3181,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',
Expand Down Expand Up @@ -3222,6 +3315,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'),
Expand Down Expand Up @@ -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'),
Expand Down
44 changes: 44 additions & 0 deletions tests/integration/targets/docker_container/tasks/tests/options.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ########################################################
####################################################################
Expand Down