diff --git a/changelogs/fragments/47814-docker_container-device-io-limit-parameters.yaml b/changelogs/fragments/47814-docker_container-device-io-limit-parameters.yaml new file mode 100644 index 00000000000..687d483b718 --- /dev/null +++ b/changelogs/fragments/47814-docker_container-device-io-limit-parameters.yaml @@ -0,0 +1,2 @@ +minor_changes: +- "docker_container - Add support for device I/O rate limit parameters. This includes ``device_read_bps``, ``device_write_bps``, ``device_read_iops`` and ``device_write_iops``" diff --git a/lib/ansible/modules/cloud/docker/docker_container.py b/lib/ansible/modules/cloud/docker/docker_container.py index 507900eeaea..d9ea24c4fbd 100644 --- a/lib/ansible/modules/cloud/docker/docker_container.py +++ b/lib/ansible/modules/cloud/docker/docker_container.py @@ -100,6 +100,38 @@ options: description: - "List of host device bindings to add to the container. Each binding is a mapping expressed in the format: ::" + device_read_bps: + description: + - "List of device path and read rate (bytes per second) from device." + - "I(path) - device path in the container." + - "I(rate) - device read limit (format: [])" + - "Number is a positive integer. Unit can be one of C(B) (byte), C(K) (kibibyte, 1024B), C(M) (mebibyte), C(G) (gibibyte), + C(T) (tebibyte), or C(P) (pebibyte)" + - "Omitting the unit defaults to bytes." + version_added: "2.8" + device_write_bps: + description: + - "List of device and write rate (bytes per second) to device." + - "I(path) - device path in the container." + - "I(rate) - device write limit (format: [])" + - "Number is a positive integer. Unit can be one of C(B) (byte), C(K) (kibibyte, 1024B), C(M) (mebibyte), C(G) (gibibyte), + C(T) (tebibyte), or C(P) (pebibyte)" + - "Omitting the unit defaults to bytes." + version_added: "2.8" + device_read_iops: + description: + - "List of device and read rate (IO per second) from device." + - "I(path) - device path in the container." + - "I(rate) - device read limit (format: )" + - "Number is a positive integer." + version_added: "2.8" + device_write_iops: + description: + - "List of device and write rate (IO per second) to device." + - "I(path) - device path in the container." + - "I(rate) - device write limit (format: )" + - "Number is a positive integer." + version_added: "2.8" dns_opts: description: - list of DNS options @@ -697,6 +729,20 @@ EXAMPLES = ''' healthcheck: # The "NONE" check needs to be specified test: ["NONE"] + +- name: start container with block device read limit + docker_container: + name: test + image: ubuntu:18.04 + state: started + device_read_bps: + # Limit read rate for /dev/sda to 20 mebibytes per second + - path: /dev/sda + rate: 20M + device_read_iops: + # Limit read rate for /dev/sdb to 300 IO per second + - path: /dev/sdb + rate: 300 ''' RETURN = ''' @@ -853,6 +899,10 @@ class TaskParameters(DockerBaseClass): self.detach = None self.debug = None self.devices = None + self.device_read_bps = None + self.device_write_bps = None + self.device_read_iops = None + self.device_write_iops = None self.dns_servers = None self.dns_opts = None self.dns_search_domains = None @@ -990,6 +1040,14 @@ class TaskParameters(DockerBaseClass): if isinstance(self.command, list): self.command = ' '.join([str(x) for x in self.command]) + for param_name in ["device_read_bps", "device_write_bps"]: + if client.module.params.get(param_name): + self._process_rate_bps(option=param_name) + + for param_name in ["device_read_iops", "device_write_iops"]: + if client.module.params.get(param_name): + self._process_rate_iops(option=param_name) + def fail(self, msg): self.client.module.fail_json(msg=msg) @@ -1171,6 +1229,13 @@ class TaskParameters(DockerBaseClass): if self.client.HAS_RUNTIME_OPT: host_config_params['runtime'] = 'runtime' + if self.client.HAS_DEVICE_RW_LIMIT_OPT: + # device_read/write_bps/iops are only supported in docker>=1.9 and docker-api>=1.22 + host_config_params['device_read_bps'] = 'device_read_bps' + host_config_params['device_write_bps'] = 'device_write_bps' + host_config_params['device_read_iops'] = 'device_read_iops' + host_config_params['device_write_iops'] = 'device_write_iops' + params = dict() for key, value in host_config_params.items(): if getattr(self, value, None) is not None: @@ -1488,6 +1553,33 @@ class TaskParameters(DockerBaseClass): self.fail("Error getting network id for %s - %s" % (network_name, str(exc))) return network_id + def _process_rate_bps(self, option): + """ + Format device_read_bps and device_write_bps option + """ + devices_list = [] + for v in getattr(self, option): + device_dict = dict((x.title(), y) for x, y in v.items()) + device_dict['Rate'] = human_to_bytes(device_dict.get('Rate', 0)) + devices_list.append(device_dict) + + setattr(self, option, devices_list) + + def _process_rate_iops(self, option): + """ + Format device_read_iops and device_write_iops option + """ + devices_list = [] + for v in getattr(self, option): + try: + device_dict = dict((x.title(), y) for x, y in v.items()) + device_dict['Rate'] = int(device_dict.get('Rate', 0)) + devices_list.append(device_dict) + except ValueError: + self.fail("Invalid device iops value: '{0}'. Must be a positive integer.".format(device_dict.get('Rate'))) + + setattr(self, option, devices_list) + class Container(DockerBaseClass): @@ -1523,6 +1615,10 @@ class Container(DockerBaseClass): self.parameters_map['expected_cmd'] = 'command' self.parameters_map['expected_devices'] = 'devices' self.parameters_map['expected_healthcheck'] = 'healthcheck' + self.parameters_map['device_read_bps'] = 'device_read_bps' + self.parameters_map['device_write_bps'] = 'device_write_bps' + self.parameters_map['device_read_iops'] = 'device_read_iops' + self.parameters_map['device_write_iops'] = 'device_write_iops' def fail(self, msg): self.parameters.client.module.fail_json(msg=msg) @@ -1642,6 +1738,10 @@ class Container(DockerBaseClass): publish_all_ports=host_config.get('PublishAllPorts'), expected_healthcheck=config.get('Healthcheck'), disable_healthcheck=(not config.get('Healthcheck') or config.get('Healthcheck').get('Test') == ['NONE']), + device_read_bps=host_config.get('BlkioDeviceReadBps'), + device_write_bps=host_config.get('BlkioDeviceWriteBps'), + device_read_iops=host_config.get('BlkioDeviceReadIOps'), + device_write_iops=host_config.get('BlkioDeviceWriteIOps'), ) if self.parameters.restart_policy: config_mapping['restart_retries'] = restart_policy.get('MaximumRetryCount') @@ -2394,6 +2494,10 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): entrypoint='list', etc_hosts='set', ulimits='set(dict)', + device_read_bps='set(dict)', + device_write_bps='set(dict)', + device_read_iops='set(dict)', + device_write_iops='set(dict)', ) all_options = set() # this is for improving user feedback when a wrong option was specified for comparison default_values = dict( @@ -2539,12 +2643,28 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): if self.module.params.get("healthcheck") and not healthcheck_supported: self.fail("docker or docker-py version is %s. Minimum version required is 2.0 to set healthcheck option." % (docker_version,)) + found_device_limit_param = False + for x in ["device_read_bps", "device_write_bps", "device_read_iops", "device_write_iops"]: + if self.module.params.get(x): + found_device_limit_param = True + break + + device_rw_limit_supported = LooseVersion(docker_api_version) >= LooseVersion('1.22') + if found_device_limit_param and not device_rw_limit_supported: + self.fail('docker API version is %s. Minimum version required is 1.22 to set device IO limit options.' % (docker_api_version,)) + + device_rw_limit_supported = device_rw_limit_supported and LooseVersion(docker_version) >= LooseVersion('1.9.0') + if found_device_limit_param and not device_rw_limit_supported: + self.fail("docker or docker-py version is %s. Minimum version required is 1.9 to set device IO limit optons. " + "If you use the 'docker-py' module, you have to switch to the docker 'Python' package." % (docker_version,)) + self.HAS_INIT_OPT = init_supported self.HAS_UTS_MODE_OPT = uts_mode_supported self.HAS_BLKIO_WEIGHT_OPT = blkio_weight_supported self.HAS_CPUSET_MEMS_OPT = cpuset_mems_supported self.HAS_STOP_TIMEOUT_OPT = stop_timeout_supported self.HAS_HEALTHCHECK_OPT = healthcheck_supported + self.HAS_DEVICE_RW_LIMIT_OPT = device_rw_limit_supported self.HAS_AUTO_REMOVE_OPT = HAS_DOCKER_PY_2 or HAS_DOCKER_PY_3 self.HAS_RUNTIME_OPT = runtime_supported @@ -2571,6 +2691,10 @@ def main(): cpu_shares=dict(type='int'), detach=dict(type='bool', default=True), devices=dict(type='list'), + device_read_bps=dict(type='list'), + device_write_bps=dict(type='list'), + device_read_iops=dict(type='list'), + device_write_iops=dict(type='list'), dns_servers=dict(type='list'), dns_opts=dict(type='list'), dns_search_domains=dict(type='list'), diff --git a/test/integration/targets/docker_container/tasks/tests/options.yml b/test/integration/targets/docker_container/tasks/tests/options.yml index a06ce32d9a9..9b7ed8bd002 100644 --- a/test/integration/targets/docker_container/tasks/tests/options.yml +++ b/test/integration/targets/docker_container/tasks/tests/options.yml @@ -586,6 +586,201 @@ - devices_3 is not changed - devices_4 is changed +#################################################################### +## device_read_bps ################################################# +#################################################################### + +- name: device_read_bps + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_read_bps: + - path: /dev/random + rate: 20M + - path: /dev/urandom + rate: 10K + register: device_read_bps_1 + +- name: device_read_bps (idempotency) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_read_bps: + - path: /dev/urandom + rate: 10K + - path: /dev/random + rate: 20M + register: device_read_bps_2 + +- name: device_read_bps (lesser entries) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_read_bps: + - path: /dev/random + rate: 20M + register: device_read_bps_3 + +- name: device_read_bps (changed) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_read_bps: + - path: /dev/random + rate: 10M + - path: /dev/urandom + rate: 5K + stop_timeout: 1 + register: device_read_bps_4 + +- name: cleanup + docker_container: + name: "{{ cname }}" + state: absent + stop_timeout: 1 + +- assert: + that: + - device_read_bps_1 is changed + - device_read_bps_2 is not changed + - device_read_bps_3 is not changed + - device_read_bps_4 is changed + +#################################################################### +## device_read_iops ################################################ +#################################################################### + +- name: device_read_iops + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_read_iops: + - path: /dev/random + rate: 10 + - path: /dev/urandom + rate: 20 + register: device_read_iops_1 + +- name: device_read_iops (idempotency) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_read_iops: + - path: /dev/urandom + rate: 20 + - path: /dev/random + rate: 10 + register: device_read_iops_2 + +- name: device_read_iops (less) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_read_iops: + - path: /dev/random + rate: 10 + register: device_read_iops_3 + +- name: device_read_iops (changed) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_read_iops: + - path: /dev/random + rate: 30 + - path: /dev/urandom + rate: 50 + stop_timeout: 1 + register: device_read_iops_4 + +- name: cleanup + docker_container: + name: "{{ cname }}" + state: absent + stop_timeout: 1 + +- assert: + that: + - device_read_iops_1 is changed + - device_read_iops_2 is not changed + - device_read_iops_3 is not changed + - device_read_iops_4 is changed + +#################################################################### +## device_write_bps and device_write_iops ########################## +#################################################################### + +- name: device_write_bps and device_write_iops + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_write_bps: + - path: /dev/random + rate: 10M + device_write_iops: + - path: /dev/urandom + rate: 30 + register: device_write_limit_1 + +- name: device_write_bps and device_write_iops (idempotency) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_write_bps: + - path: /dev/random + rate: 10M + device_write_iops: + - path: /dev/urandom + rate: 30 + register: device_write_limit_2 + +- name: device_write_bps device_write_iops (changed) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + device_write_bps: + - path: /dev/random + rate: 20K + device_write_iops: + - path: /dev/urandom + rate: 100 + stop_timeout: 1 + register: device_write_limit_3 + +- name: cleanup + docker_container: + name: "{{ cname }}" + state: absent + stop_timeout: 1 + +- assert: + that: + - device_write_limit_1 is changed + - device_write_limit_2 is not changed + - device_write_limit_3 is changed + #################################################################### ## dns_opts ######################################################## ####################################################################