diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index bc4302e29a..49af58ea41 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -108,6 +108,11 @@ docker_common_options: restart_policy: "{{ docker_restart_policy }}" restart_retries: "{{ docker_restart_policy_retry }}" +#################### +# Dimensions options +#################### +# Dimension options for Docker Containers +default_container_dimensions: {} #################### # keepalived options diff --git a/ansible/library/kolla_docker.py b/ansible/library/kolla_docker.py index f736dd7bf6..4d4fe73145 100644 --- a/ansible/library/kolla_docker.py +++ b/ansible/library/kolla_docker.py @@ -552,6 +552,18 @@ class DockerWorker(object): 'volumes_from': self.params.get('volumes_from') } + if self.params.get('dimensions'): + supported = {'cpu_period', 'cpu_quota', 'cpu_shares', + 'cpuset_cpus', 'cpuset_mems', 'mem_limit', + 'mem_reservation', 'memswap_limit', + 'kernel_memory', 'blkio_weight'} + unsupported = set(self.params.get('dimensions')) - supported + if unsupported: + self.module.exit_json(failed=True, + msg=repr("Unsupported dimensions"), + unsupported_dimensions=unsupported) + options.update(self.params.get('dimensions')) + if self.params.get('restart_policy') in ['on-failure', 'always', 'unless-stopped']: @@ -772,7 +784,8 @@ def generate_module(): tls_key=dict(required=False, type='str'), tls_cacert=dict(required=False, type='str'), volumes=dict(required=False, type='list'), - volumes_from=dict(required=False, type='list') + volumes_from=dict(required=False, type='list'), + dimensions=dict(required=False, type='dict', default=dict()) ) required_if = [ ['action', 'pull_image', ['image']], diff --git a/ansible/roles/nova/defaults/main.yml b/ansible/roles/nova/defaults/main.yml index 8a22cbffdf..92a8625238 100644 --- a/ansible/roles/nova/defaults/main.yml +++ b/ansible/roles/nova/defaults/main.yml @@ -21,6 +21,7 @@ nova_services: - "nova_compute:/var/lib/nova/" - "{% if enable_cinder_backend_nfs | bool %}/var/lib/nova/mnt:/var/lib/nova/mnt:shared{% endif %}" - "nova_libvirt_qemu:/etc/libvirt/qemu" + dimensions: "{{ nova_libvirt_dimensions }}" nova-ssh: container_name: "nova_ssh" group: "compute" @@ -32,6 +33,7 @@ nova_services: - "kolla_logs:/var/log/kolla" - "nova_compute:/var/lib/nova" - "{% if enable_cinder_backend_nfs | bool %}/var/lib/nova/mnt:/var/lib/nova/mnt:shared{% endif %}" + dimensions: "{{ nova_ssh_dimensions }}" placement-api: container_name: "placement_api" group: "placement-api" @@ -41,6 +43,7 @@ nova_services: - "{{ node_config_directory }}/placement-api/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" - "kolla_logs:/var/log/kolla/" + dimensions: "{{ placement_api_dimensions }}" nova-api: container_name: "nova_api" group: "nova-api" @@ -52,6 +55,7 @@ nova_services: - "/etc/localtime:/etc/localtime:ro" - "/lib/modules:/lib/modules:ro" - "kolla_logs:/var/log/kolla/" + dimensions: "{{ nova_api_dimensions }}" nova-consoleauth: container_name: "nova_consoleauth" group: "nova-consoleauth" @@ -61,6 +65,7 @@ nova_services: - "{{ node_config_directory }}/nova-consoleauth/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" - "kolla_logs:/var/log/kolla/" + dimensions: "{{ nova_consoleauth_dimensions }}" nova-novncproxy: container_name: "nova_novncproxy" group: "nova-novncproxy" @@ -70,6 +75,7 @@ nova_services: - "{{ node_config_directory }}/nova-novncproxy/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" - "kolla_logs:/var/log/kolla/" + dimensions: "{{ nova_novncproxy_dimensions }}" nova-scheduler: container_name: "nova_scheduler" group: "nova-scheduler" @@ -79,6 +85,7 @@ nova_services: - "{{ node_config_directory }}/nova-scheduler/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" - "kolla_logs:/var/log/kolla/" + dimensions: "{{ nova_scheduler_dimensions }}" nova-spicehtml5proxy: container_name: "nova_spicehtml5proxy" group: "nova-spicehtml5proxy" @@ -88,6 +95,7 @@ nova_services: - "{{ node_config_directory }}/nova-spicehtml5proxy/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" - "kolla_logs:/var/log/kolla/" + dimensions: "{{ nova_spicehtml5proxy_dimensions }}" nova-serialproxy: container_name: "nova_serialproxy" group: "nova-serialproxy" @@ -97,6 +105,7 @@ nova_services: - "{{ node_config_directory }}/nova-serialproxy/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" - "kolla_logs:/var/log/kolla/" + dimensions: "{{ nova_serialproxy_dimensions }}" nova-conductor: container_name: "nova_conductor" group: "nova-conductor" @@ -106,6 +115,7 @@ nova_services: - "{{ node_config_directory }}/nova-conductor/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" - "kolla_logs:/var/log/kolla/" + dimensions: "{{ nova_conductor_dimensions }}" nova-compute: container_name: "nova_compute" group: "compute" @@ -126,6 +136,7 @@ nova_services: - "libvirtd:/var/lib/libvirt" - "nova_compute:/var/lib/nova/" - "{% if enable_cinder_backend_nfs | bool %}/var/lib/nova/mnt:/var/lib/nova/mnt:shared{% endif %}" + dimensions: "{{ nova_compute_dimensions }}" nova-compute-ironic: container_name: "nova_compute_ironic" group: "nova-compute-ironic" @@ -135,6 +146,7 @@ nova_services: - "{{ node_config_directory }}/nova-compute-ironic/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" - "kolla_logs:/var/log/kolla/" + dimensions: "{{ nova_compute_ironic_dimensions }}" #################### # Ceph @@ -232,6 +244,19 @@ placement_api_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ d placement_api_tag: "{{ nova_tag }}" placement_api_image_full: "{{ placement_api_image }}:{{ placement_api_tag }}" +nova_libvirt_dimensions: "{{ default_container_dimensions }}" +nova_ssh_dimensions: "{{ default_container_dimensions }}" +placement_api_dimensions: "{{ default_container_dimensions }}" +nova_api_dimensions: "{{ default_container_dimensions }}" +nova_consoleauth_dimensions: "{{ default_container_dimensions }}" +nova_novncproxy_dimensions: "{{ default_container_dimensions }}" +nova_scheduler_dimensions: "{{ default_container_dimensions }}" +nova_spicehtml5proxy_dimensions: "{{ default_container_dimensions }}" +nova_serialproxy_dimensions: "{{ default_container_dimensions }}" +nova_conductor_dimensions: "{{ default_container_dimensions }}" +nova_compute_dimensions: "{{ default_container_dimensions }}" +nova_compute_ironic_dimensions: "{{ default_container_dimensions }}" + #################### # OpenStack #################### diff --git a/ansible/roles/nova/handlers/main.yml b/ansible/roles/nova/handlers/main.yml index 3e037554da..079c253d72 100644 --- a/ansible/roles/nova/handlers/main.yml +++ b/ansible/roles/nova/handlers/main.yml @@ -14,6 +14,7 @@ pid_mode: "{{ service.pid_mode | default('') }}" privileged: "{{ service.privileged | default(False) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] @@ -37,6 +38,7 @@ pid_mode: "{{ service.pid_mode | default('') }}" privileged: "{{ service.privileged | default(False) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" register: restart_nova_libvirt # NOTE(Jeffrey4l): retry 5 to remove nova_libvirt container because when # guests running, nova_libvirt will raise error even though it is removed. @@ -65,6 +67,7 @@ name: "{{ service.container_name }}" image: "{{ service.image }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] @@ -91,6 +94,7 @@ image: "{{ service.image }}" privileged: "{{ service.privileged | default(False) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] @@ -116,6 +120,7 @@ image: "{{ service.image }}" privileged: "{{ service.privileged | default(False) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] @@ -141,6 +146,7 @@ image: "{{ service.image }}" privileged: "{{ service.privileged | default(False) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] @@ -167,6 +173,7 @@ image: "{{ service.image }}" privileged: "{{ service.privileged | default(False) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] @@ -192,6 +199,7 @@ image: "{{ service.image }}" privileged: "{{ service.privileged | default(False) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] @@ -217,6 +225,7 @@ image: "{{ service.image }}" privileged: "{{ service.privileged | default(False) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] @@ -242,6 +251,7 @@ image: "{{ service.image }}" privileged: "{{ service.privileged | default(False) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] @@ -269,6 +279,7 @@ privileged: "{{ service.privileged | default(False) }}" ipc_mode: "{{ service.ipc_mode | default(omit) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] @@ -295,6 +306,7 @@ image: "{{ service.image }}" privileged: "{{ service.privileged | default(False) }}" volumes: "{{ service.volumes|reject('equalto', '')|list }}" + dimensions: "{{ service.dimensions }}" when: - kolla_action != "config" - inventory_hostname in groups[service.group] diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst index b9d041d6df..7427439c8f 100644 --- a/doc/source/reference/index.rst +++ b/doc/source/reference/index.rst @@ -27,3 +27,4 @@ Projects Deployment References tacker-guide xenserver-guide horizon-guide + resource-constraints diff --git a/doc/source/reference/resource-constraints.rst b/doc/source/reference/resource-constraints.rst new file mode 100644 index 0000000000..875db5bd83 --- /dev/null +++ b/doc/source/reference/resource-constraints.rst @@ -0,0 +1,84 @@ +.. _resource-constraints: + +===================================== +Resource Constraints in Kolla Ansible +===================================== + +Overview +~~~~~~~~ + +Since the Rocky release it is possible to restrict +the resource usage of deployed containers. +The following components support this feature: + +* Nova + +In Kolla Ansible, +container resources to be constrained are referred to as dimensions. + +The `Docker documentation `__ +provides information on container resource constraints. +The resources currently supported by Kolla Ansible are: + +.. code-block:: console + + cpu_period + cpu_quota + cpu_shares + cpuset_cpus + cpuset_mems + mem_limit + mem_reservation + memswap_limit + kernel_memory + blkio_weight + +.. end + +Pre-deployment Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Dimensions are defined as a mapping from a Docker resource name +(e.g. ``cpu_period``) to a string constraint value. + +The variable ``default_container_dimensions`` sets the default dimensions +for all supported containers, and by default these are unconstrained. + +Each supported container has an associated variable, +``_dimensions``, that can be used to set the resources +for the container. For example, dimensions for the ``nova_libvirt`` +container are set via the variable ``nova_libvirt_dimensions``. + +For example, +to constrain the number of CPUs that may be used by all supported containers, +add the following to the dimensions options section in +``/etc/kolla/globals.yml``: + +.. code-block:: yaml + + default_container_dimensions: + cpuset_cpus: "1" + +.. end + +For example, to constrain the number of CPUs that may be used by +the ``nova_libvirt`` container, add the following to the dimensions +options section in ``/etc/kolla/globals.yml``: + +.. code-block:: yaml + + nova_libvirt_dimensions: + cpuset_cpus: "2" + +.. end + +Deployment +~~~~~~~~~~ + +To deploy resource constrained containers, run the deployment as usual: + +.. code-block:: console + + $ kolla-ansible deploy -i /path/to/inventory + +.. end diff --git a/etc/kolla/globals.yml b/etc/kolla/globals.yml index 2e9200a2b4..571eca4b0f 100644 --- a/etc/kolla/globals.yml +++ b/etc/kolla/globals.yml @@ -113,6 +113,28 @@ kolla_internal_vip_address: "10.10.10.254" # Arbitrary unique number from 0..255 #keepalived_virtual_router_id: "51" +################### +# Dimension options +################### +# This is to provide an extra option to deploy containers with Resource constraints. +# We call it dimensions here. +# The dimensions for each container are defined by a mapping, where each dimension value should be a +# string. +# Reference_Docs +# https://docs.docker.com/config/containers/resource_constraints/ +# eg: +# _dimensions: +# blkio_weight: +# cpu_period: +# cpu_quota: +# cpu_shares: +# cpuset_cpus: +# cpuset_mems: +# mem_limit: +# mem_reservation: +# memswap_limit: +# kernel_memory: + ############# # TLS options diff --git a/tests/test_kolla_docker.py b/tests/test_kolla_docker.py index f0ea08fdb7..7029856eb0 100644 --- a/tests/test_kolla_docker.py +++ b/tests/test_kolla_docker.py @@ -83,7 +83,8 @@ class ModuleArgsTest(base.BaseTestCase): tls_key=dict(required=False, type='str'), tls_cacert=dict(required=False, type='str'), volumes=dict(required=False, type='list'), - volumes_from=dict(required=False, type='list') + volumes_from=dict(required=False, type='list'), + dimensions=dict(required=False, type='dict', default=dict()) ) required_if = [ ['action', 'pull_image', ['image']], @@ -191,14 +192,38 @@ class TestContainer(base.BaseTestCase): super(TestContainer, self).setUp() self.fake_data = copy.deepcopy(FAKE_DATA) - def test_create_container(self): + def test_create_container_without_dimensions(self): self.dw = get_DockerWorker(self.fake_data['params']) self.dw.dc.create_host_config = mock.MagicMock( return_value=self.fake_data['params']['host_config']) self.dw.create_container() self.assertTrue(self.dw.changed) + + def test_create_container_with_dimensions(self): + self.fake_data['params']['dimensions'] = {'blkio_weight': 10} + self.dw = get_DockerWorker(self.fake_data['params']) + self.dw.dc.create_host_config = mock.MagicMock( + return_value=self.fake_data['params']['host_config']) + self.dw.create_container() + self.assertTrue(self.dw.changed) + self.fake_data['params'].pop('dimensions') + self.fake_data['params']['host_config']['blkio_weight'] = '10' self.dw.dc.create_container.assert_called_once_with( **self.fake_data['params']) + self.dw.dc.create_host_config.assert_called_with( + cap_add=None, network_mode='host', ipc_mode=None, + pid_mode=None, volumes_from=None, blkio_weight=10, + security_opt=None, privileged=None) + + def test_create_container_wrong_dimensions(self): + self.fake_data['params']['dimensions'] = {'random': 10} + self.dw = get_DockerWorker(self.fake_data['params']) + self.dw.dc.create_host_config = mock.MagicMock( + return_value=self.fake_data['params']['host_config']) + self.dw.create_container() + self.dw.module.exit_json.assert_called_once_with( + failed=True, msg=repr("Unsupported dimensions"), + unsupported_dimensions=set(['random'])) def test_start_container_without_pull(self): self.fake_data['params'].update({'auth_username': 'fake_user',