diff --git a/tox.ini b/tox.ini index e5828f7f6..3a98bcbee 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,8 @@ commands = bash tools/flake8wrap.sh {posargs} # The following bandit tests are being skipped: # B303 - Use of insecure MD2, MD4, or MD5 hash function. - bandit -r zun -x tests -n5 -ll --skip B303 + # B604 - unction call with shell=True parameter identified, possible security issue. + bandit -r zun -x tests -n5 -ll --skip B303,B604 [testenv:venv] #set PYTHONHASHSEED=0 to prevent oslo_policy.sphinxext from randomly failing. diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index 8ce06b9cb..f6cbdf915 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -387,9 +387,8 @@ class ContainersController(base.Controller): return view.format_container(pecan.request.host_url, new_container) def _set_default_resource_limit(self, container_dict): - if CONF.default_disk >= 0: - container_dict['disk'] = container_dict.get( - 'disk', CONF.default_disk) + # NOTE(kiennt): Default disk size will be set later. + container_dict['disk'] = container_dict.get('disk') container_dict['memory'] = container_dict.get( 'memory', CONF.default_memory) container_dict['memory'] = str(container_dict['memory']) + 'M' diff --git a/zun/common/consts.py b/zun/common/consts.py index 75060da87..250a13245 100644 --- a/zun/common/consts.py +++ b/zun/common/consts.py @@ -23,7 +23,7 @@ CAPSULE_STATUSES = ( PENDING, RUNNING, SUCCEEDED, FAILED, UNKNOWN ) = ( 'Pending', 'Running', 'Succeeded', 'Failed', 'Unknown' - ) +) TASK_STATES = ( IMAGE_PULLING, CONTAINER_CREATING, SANDBOX_CREATING, @@ -54,3 +54,7 @@ ALLOCATED = 'allocated' # The name of Docker container is of the form NAME_PREFIX- NAME_PREFIX = 'zun-' SANDBOX_NAME_PREFIX = 'zun-sandbox-' + +# Storage drivers that support disk quota feature +SUPPORTED_STORAGE_DRIVERS = \ + ['devicemapper', 'overlay2', 'windowfilter', 'zfs', 'btrfs'] diff --git a/zun/compute/manager.py b/zun/compute/manager.py index 475d7c4fa..c230c186b 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -182,6 +182,39 @@ class Manager(periodic_task.PeriodicTasks): self._fail_container(context, container, msg, unset_host=True) raise exception.Conflict(msg) + def _check_support_disk_quota(self, context, container): + base_device_size = self.driver.get_host_default_base_size() + if base_device_size: + # NOTE(kiennt): If default_base_size is not None, it means + # host storage_driver is in list ['devicemapper', + # windowfilter', 'zfs', 'btrfs']. The following + # block is to prevent Zun raises Exception everytime + # if user do not set container's disk and + # default_disk less than base_device_size. + # FIXME(kiennt): This block is too complicated. We should find + # new effecient way to do the check. + if not container.disk: + container.disk = max(base_device_size, CONF.default_disk) + return + else: + if container.disk < base_device_size: + msg = _('Disk size cannot be smaller than ' + '%(base_device_size)s.') % { + 'base_device_size': base_device_size + } + self._fail_container(context, container, + msg, unset_host=True) + raise exception.Invalid(msg) + # NOTE(kiennt): Only raise Exception when user passes disk size and + # the disk quota feature isn't supported in host. + if not self.driver.node_support_disk_quota() and container.disk: + msg = _('Your host does not support disk quota feature.') + self._fail_container(context, container, msg, unset_host=True) + raise exception.Invalid(msg) + if self.driver.node_support_disk_quota() and not container.disk: + container.disk = CONF.default_disk + return + def container_create(self, context, limits, requested_networks, requested_volumes, container, run, pci_requests=None): @utils.synchronized(container.uuid) @@ -189,6 +222,7 @@ class Manager(periodic_task.PeriodicTasks): self._wait_for_volumes_available(context, requested_volumes, container) self._attach_volumes(context, container, requested_volumes) + self._check_support_disk_quota(context, container) created_container = self._do_container_create( context, container, requested_networks, requested_volumes, pci_requests, limits) diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index 38c200cb7..aec06af64 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -14,13 +14,13 @@ import datetime import eventlet import functools -import six import types from docker import errors from oslo_log import log as logging from oslo_utils import timeutils from oslo_utils import uuidutils +import six from zun.common import consts from zun.common import exception @@ -98,6 +98,12 @@ class DockerDriver(driver.ContainerDriver): def __init__(self): super(DockerDriver, self).__init__() self._host = host.Host() + self._get_host_storage_info() + + def _get_host_storage_info(self): + storage_info = self._host.get_storage_info() + self.base_device_size = storage_info['default_base_size'] + self.support_disk_quota = self._host.check_supported_disk_quota() def load_image(self, image_path=None): with docker_utils.docker_client() as docker: @@ -187,6 +193,7 @@ class DockerDriver(driver.ContainerDriver): name = container.restart_policy['Name'] host_config['restart_policy'] = {'Name': name, 'MaximumRetryCount': count} + if container.disk: disk_size = str(container.disk) + 'G' host_config['storage_opt'] = {'size': disk_size} @@ -208,6 +215,12 @@ class DockerDriver(driver.ContainerDriver): container.save(context) return container + def node_support_disk_quota(self): + return self.support_disk_quota + + def get_host_default_base_size(self): + return self.base_device_size + def _process_networking_config(self, context, container, requested_networks, host_config, container_kwargs, docker): diff --git a/zun/container/docker/host.py b/zun/container/docker/host.py index 43fe59627..a8b23839c 100644 --- a/zun/container/docker/host.py +++ b/zun/container/docker/host.py @@ -17,6 +17,9 @@ Manages information about the host. from oslo_log import log as logging +from zun.common import consts +from zun.common import exception +from zun.common import utils from zun.container.docker import utils as docker_utils LOG = logging.getLogger(__name__) @@ -40,3 +43,43 @@ class Host(object): 'to take effect.', {'old': self._hostname, 'new': hostname}) return self._hostname + + def get_storage_info(self): + with docker_utils.docker_client() as docker: + info = docker.info() + storage_driver = str(info['Driver']) + # DriverStatus is list. Convert it to dict + driver_status = dict(info['DriverStatus']) + backing_filesystem = \ + str(driver_status.get('Backing Filesystem')) + default_base_size = driver_status.get('Base Device Size') + if default_base_size: + default_base_size = float(default_base_size.strip('GB')) + return { + 'storage_driver': storage_driver, + 'backing_filesystem': backing_filesystem, + 'default_base_size': default_base_size + } + + def check_supported_disk_quota(self): + """Check your system be supported disk quota or not""" + storage_info = self.get_storage_info() + sp_disk_quota = True + storage_driver = storage_info['storage_driver'] + backing_filesystem = storage_info['backing_filesystem'] + if storage_driver not in consts.SUPPORTED_STORAGE_DRIVERS: + sp_disk_quota = False + else: + if storage_driver == 'overlay2': + if backing_filesystem == 'xfs': + # Check project quota mount option + try: + utils.execute( + "mount | grep $(df /var/lib/docker | " + "awk 'FNR==2 {print $1}') |grep 'xfs' |" + " grep 'pquota'", shell=True) + except exception.CommandError: + self.sp_disk_quota = False + else: + sp_disk_quota = False + return sp_disk_quota diff --git a/zun/container/driver.py b/zun/container/driver.py index 3abc08daf..74b685809 100644 --- a/zun/container/driver.py +++ b/zun/container/driver.py @@ -258,3 +258,9 @@ class ContainerDriver(object): def network_attach(self, context, container, network): raise NotImplementedError() + + def node_support_disk_quota(self): + raise NotImplementedError() + + def get_host_default_base_size(self): + raise NotImplementedError() diff --git a/zun/tests/unit/container/docker/test_docker_driver.py b/zun/tests/unit/container/docker/test_docker_driver.py index cc79819ec..66298258c 100644 --- a/zun/tests/unit/container/docker/test_docker_driver.py +++ b/zun/tests/unit/container/docker/test_docker_driver.py @@ -44,7 +44,10 @@ _numa_topo_spec = [_numa_node] class TestDockerDriver(base.DriverTestCase): - def setUp(self): + + @mock.patch('zun.container.docker.driver.DockerDriver.' + '_get_host_storage_info') + def setUp(self, mock_get): super(TestDockerDriver, self).setUp() self.driver = DockerDriver() dfc_patcher = mock.patch.object(docker_utils, 'docker_client') @@ -92,10 +95,11 @@ class TestDockerDriver(base.DriverTestCase): '.create_or_update_port') @mock.patch('zun.common.utils.get_security_group_ids') @mock.patch('zun.objects.container.Container.save') - def test_create_image_path_is_none(self, mock_save, - mock_get_security_group_ids, - mock_create_or_update_port, - mock_connect): + def test_create_image_path_is_none_with_overlay2( + self, mock_save, + mock_get_security_group_ids, + mock_create_or_update_port, + mock_connect): self.mock_docker.create_host_config = mock.Mock( return_value={'Id1': 'val1', 'key2': 'val2'}) self.mock_docker.create_container = mock.Mock( @@ -112,6 +116,75 @@ class TestDockerDriver(base.DriverTestCase): volumes = [] fake_port = {'mac_address': 'fake_mac'} mock_create_or_update_port.return_value = ([], fake_port) + # DockerDriver with supported storage driver - overlay2 + self.driver._host.sp_disk_quota = True + self.driver._host.storage_driver = 'overlay2' + result_container = self.driver.create(self.context, mock_container, + image, networks, volumes) + host_config = {} + host_config['mem_limit'] = '512m' + host_config['cpu_quota'] = 100000 + host_config['cpu_period'] = 100000 + host_config['restart_policy'] = {'Name': 'no', 'MaximumRetryCount': 0} + host_config['runtime'] = 'runc' + host_config['binds'] = {} + host_config['network_mode'] = 'fake-network' + host_config['storage_opt'] = {'size': '20G'} + self.mock_docker.create_host_config.assert_called_once_with( + **host_config) + + kwargs = { + 'name': '%sea8e2a25-2901-438d-8157-de7ffd68d051' % + consts.NAME_PREFIX, + 'command': 'fake_command', + 'environment': {'key1': 'val1', 'key2': 'val2'}, + 'working_dir': '/home/ubuntu', + 'labels': {'key1': 'val1', 'key2': 'val2'}, + 'host_config': {'Id1': 'val1', 'key2': 'val2'}, + 'stdin_open': True, + 'tty': True, + 'hostname': 'testhost', + 'volumes': [], + 'networking_config': {'Id': 'val1', 'key1': 'val2'}, + 'mac_address': 'fake_mac', + } + self.mock_docker.create_container.assert_called_once_with( + image['repo'] + ":" + image['tag'], **kwargs) + self.assertEqual('val1', result_container.container_id) + self.assertEqual(result_container.status, + consts.CREATED) + + @mock.patch('zun.network.kuryr_network.KuryrNetwork' + '.connect_container_to_network') + @mock.patch('zun.network.kuryr_network.KuryrNetwork' + '.create_or_update_port') + @mock.patch('zun.common.utils.get_security_group_ids') + @mock.patch('zun.objects.container.Container.save') + def test_create_image_path_is_none_with_devicemapper( + self, mock_save, + mock_get_security_group_ids, + mock_create_or_update_port, + mock_connect): + self.mock_docker.create_host_config = mock.Mock( + return_value={'Id1': 'val1', 'key2': 'val2'}) + self.mock_docker.create_container = mock.Mock( + return_value={'Id': 'val1', 'key1': 'val2'}) + self.mock_docker.create_networking_config = mock.Mock( + return_value={'Id': 'val1', 'key1': 'val2'}) + self.mock_docker.inspect_container = mock.Mock( + return_value={'State': 'created', + 'Config': {'Cmd': ['fake_command']}}) + image = {'path': '', 'image': '', 'repo': 'test', 'tag': 'test'} + mock_container = self.mock_default_container + mock_container.status = 'Creating' + networks = [{'network': 'fake-network'}] + volumes = [] + fake_port = {'mac_address': 'fake_mac'} + mock_create_or_update_port.return_value = ([], fake_port) + # DockerDriver with supported storage driver - overlay2 + self.driver._host.sp_disk_quota = True + self.driver._host.storage_driver = 'devicemapper' + self.driver._host.default_base_size = 10 result_container = self.driver.create(self.context, mock_container, image, networks, volumes) host_config = {} diff --git a/zun/tests/unit/container/fake_driver.py b/zun/tests/unit/container/fake_driver.py index 48ae25854..d81b54caf 100644 --- a/zun/tests/unit/container/fake_driver.py +++ b/zun/tests/unit/container/fake_driver.py @@ -115,3 +115,9 @@ class FakeDriver(driver.ContainerDriver): def check_container_exist(self, context): pass + + def node_support_disk_quota(self): + return True + + def get_host_default_base_size(self): + return None