diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index 6e6be5062..4072aad7d 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -35,6 +35,7 @@ from zun.common import validation import zun.conf from zun.network import neutron from zun import objects +from zun.volume import cinder_api as cinder CONF = zun.conf.CONF LOG = logging.getLogger(__name__) @@ -242,6 +243,17 @@ class ContainersController(base.Controller): nets = container_dict.get('nets', []) requested_networks = self._build_requested_networks(context, nets) + mounts = container_dict.pop('mounts', []) + if mounts: + req_version = pecan.request.version + min_version = versions.Version('', '', '', '1.11') + if req_version < min_version: + raise exception.InvalidParamInVersion(param='mounts', + req_version=req_version, + min_version=min_version) + + requested_volumes = self._build_requested_volumes(context, mounts) + # Valiadtion accepts 'None' so need to convert it to None if container_dict.get('image_driver'): container_dict['image_driver'] = api_utils.string_or_none( @@ -275,6 +287,7 @@ class ContainersController(base.Controller): kwargs = {} kwargs['extra_spec'] = extra_spec kwargs['requested_networks'] = requested_networks + kwargs['requested_volumes'] = requested_volumes kwargs['run'] = run compute_api.container_create(context, new_container, **kwargs) # Set the HTTP Location Header @@ -331,6 +344,25 @@ class ContainersController(base.Controller): self._check_external_network_attach(context, requested_networks) return requested_networks + def _build_requested_volumes(self, context, mounts): + # NOTE(hongbin): We assume cinder is the only volume provider here. + # The logic needs to be re-visited if a second volume provider + # (i.e. Manila) is introduced. + cinder_api = cinder.CinderAPI(context) + requested_volumes = [] + for mount in mounts: + volume = cinder_api.search_volume(mount['source']) + cinder_api.ensure_volume_usable(volume) + volmapp = objects.VolumeMapping( + context, + volume_id=volume.id, volume_provider='cinder', + container_path=mount['destination'], + user_id=context.user_id, + project_id=context.project_id) + requested_volumes.append(volmapp) + + return requested_volumes + def _check_security_group(self, context, security_group, container): if security_group.get("uuid"): security_group_id = security_group.get("uuid") diff --git a/zun/api/controllers/v1/schemas/containers.py b/zun/api/controllers/v1/schemas/containers.py index c0487a323..27a8f2f24 100644 --- a/zun/api/controllers/v1/schemas/containers.py +++ b/zun/api/controllers/v1/schemas/containers.py @@ -30,6 +30,7 @@ _container_properties = { 'image_driver': parameter_types.image_driver, 'security_groups': parameter_types.security_groups, 'hints': parameter_types.hints, + 'mounts': parameter_types.mounts, 'nets': parameter_types.nets, 'runtime': parameter_types.runtime, 'hostname': parameter_types.hostname, diff --git a/zun/api/controllers/versions.py b/zun/api/controllers/versions.py index ab0133366..a92c9a131 100644 --- a/zun/api/controllers/versions.py +++ b/zun/api/controllers/versions.py @@ -43,10 +43,11 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 1.8 - Support attach a network to a container * 1.9 - Add support set container's hostname * 1.10 - Make delete container async + * 1.11 - Add mounts to container create """ BASE_VER = '1.1' -CURRENT_MAX_VER = '1.10' +CURRENT_MAX_VER = '1.11' class Version(object): diff --git a/zun/api/rest_api_version_history.rst b/zun/api/rest_api_version_history.rst index 16abefde7..75ceb9de0 100644 --- a/zun/api/rest_api_version_history.rst +++ b/zun/api/rest_api_version_history.rst @@ -86,3 +86,14 @@ user documentation. ---- Make container delete API async. Delete operation for a container can take long time, so making it async to improve user experience. + +1.11 +---- + Add a new attribute 'mounts' to the request to create a container. + Users can use this attribute to specify one or multiple mounts for + the container. Each mount could specify the source and destination. + The source is the Cinder volume id or name, and the destination is + the path where the file or directory will be mounted in the container. + For examples: + + [{'source': 'my-vol', 'destination': '/data'}] diff --git a/zun/common/validation/parameter_types.py b/zun/common/validation/parameter_types.py index 53059cf1f..6b1fcc7f0 100644 --- a/zun/common/validation/parameter_types.py +++ b/zun/common/validation/parameter_types.py @@ -113,6 +113,23 @@ nets = { 'type': ['array', 'null'] } +mounts = { + 'type': ['array', 'null'], + 'items': { + 'type': 'object', + 'properties': { + 'source': { + 'type': ['string'], + }, + 'destination': { + 'type': ['string'], + } + }, + 'additionalProperties': False, + 'required': ['source', 'destination'] + } +} + environment = { 'type': ['object', 'null'], 'patternProperties': { diff --git a/zun/compute/api.py b/zun/compute/api.py index 2493233f2..fa8063f9d 100644 --- a/zun/compute/api.py +++ b/zun/compute/api.py @@ -29,7 +29,7 @@ class API(object): super(API, self).__init__() def container_create(self, context, new_container, extra_spec, - requested_networks, run): + requested_networks, requested_volumes, run): host_state = None try: host_state = self._schedule_container(context, new_container, @@ -42,7 +42,8 @@ class API(object): self.rpcapi.container_create(context, host_state['host'], new_container, host_state['limits'], - requested_networks, run) + requested_networks, requested_volumes, + run) def _schedule_container(self, context, new_container, extra_spec): dests = self.scheduler_client.select_destinations(context, diff --git a/zun/compute/manager.py b/zun/compute/manager.py index 40ba99961..1fdab1a07 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -49,6 +49,12 @@ class Manager(periodic_task.PeriodicTasks): self.use_sandbox = False def _fail_container(self, context, container, error, unset_host=False): + try: + self._detach_volumes(context, container) + except Exception as e: + LOG.exception("Failed to detach volumes: %s", + six.text_type(e)) + container.status = consts.ERROR container.status_reason = error container.task_state = None @@ -56,16 +62,19 @@ class Manager(periodic_task.PeriodicTasks): container.host = None container.save(context) - def container_create(self, context, limits, requested_networks, container, - run): + def container_create(self, context, limits, requested_networks, + requested_volumes, container, run): @utils.synchronized(container.uuid) - def do_container_create(run, context, *args): - created_container = self._do_container_create(context, *args) + def do_container_create(): + if not self._attach_volumes(context, container, requested_volumes): + return + created_container = self._do_container_create( + context, container, requested_networks, requested_volumes, + limits) if run and created_container: self._do_container_start(context, created_container) - utils.spawn_n(do_container_create, run, context, container, - requested_networks, limits) + utils.spawn_n(do_container_create) def _do_sandbox_cleanup(self, context, container): sandbox_id = container.get_sandbox_id() @@ -83,6 +92,7 @@ class Manager(periodic_task.PeriodicTasks): container.save(context) def _do_container_create_base(self, context, container, requested_networks, + requested_volumes, sandbox=None, limits=None, reraise=False): self._update_task_state(context, container, consts.IMAGE_PULLING) repo, tag = utils.parse_image_name(container.image) @@ -127,7 +137,8 @@ class Manager(periodic_task.PeriodicTasks): with rt.container_claim(context, container, container.host, limits): container = self.driver.create(context, container, image, - requested_networks) + requested_networks, + requested_volumes) self._update_task_state(context, container, None) return container except exception.DockerError as e: @@ -148,23 +159,66 @@ class Manager(periodic_task.PeriodicTasks): return def _do_container_create(self, context, container, requested_networks, + requested_volumes, limits=None, reraise=False): LOG.debug('Creating container: %s', container.uuid) sandbox = None if self.use_sandbox: sandbox = self._create_sandbox(context, container, - requested_networks, reraise) + requested_networks, + requested_volumes, + reraise) if sandbox is None: return created_container = self._do_container_create_base(context, container, requested_networks, + requested_volumes, sandbox, limits, reraise) return created_container + def _attach_volumes(self, context, container, volumes): + try: + for volume in volumes: + volume.container_uuid = container.uuid + self._attach_volume(context, volume) + return True + except Exception as e: + with excutils.save_and_reraise_exception(reraise=False): + self._fail_container(context, container, six.text_type(e), + unset_host=True) + + def _attach_volume(self, context, volume): + volume.create(context) + context = context.elevated() + LOG.info('Attaching volume %(volume_id)s to %(host)s', + {'volume_id': volume.volume_id, + 'host': CONF.host}) + try: + self.driver.attach_volume(context, volume) + except Exception: + with excutils.save_and_reraise_exception(): + volume.destroy() + + def _detach_volumes(self, context, container, reraise=True): + volumes = objects.VolumeMapping.list_by_container(context, + container.uuid) + for volume in volumes: + self._detach_volume(context, volume, reraise=reraise) + + def _detach_volume(self, context, volume, reraise=True): + context = context.elevated() + try: + self.driver.detach_volume(context, volume) + except Exception: + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.error("Failed to detach %(volume_id)s", + {'volume_id': volume.volume_id}) + volume.destroy() + def _use_sandbox(self): if CONF.use_sandbox and self.driver.capabilities["support_sandbox"]: return True @@ -179,7 +233,7 @@ class Manager(periodic_task.PeriodicTasks): 'driver': self.driver}) def _create_sandbox(self, context, container, requested_networks, - reraise=False): + requested_volumes, reraise=False): self._update_task_state(context, container, consts.SANDBOX_CREATING) sandbox_image = CONF.sandbox_image sandbox_image_driver = CONF.sandbox_image_driver @@ -193,7 +247,8 @@ class Manager(periodic_task.PeriodicTasks): self.driver.load_image(image['path']) sandbox_id = self.driver.create_sandbox( context, container, image=sandbox_image, - requested_networks=requested_networks) + requested_networks=requested_networks, + requested_volumes=requested_volumes) return sandbox_id except Exception as e: with excutils.save_and_reraise_exception(reraise=reraise): @@ -245,6 +300,8 @@ class Manager(periodic_task.PeriodicTasks): LOG.exception("Unexpected exception: %s", six.text_type(e)) self._fail_container(context, container, six.text_type(e)) + self._detach_volumes(context, container, reraise=reraise) + self._update_task_state(context, container, None) container.destroy(context) self._get_resource_tracker() diff --git a/zun/compute/rpcapi.py b/zun/compute/rpcapi.py index 6d3f40d55..65d6b1197 100644 --- a/zun/compute/rpcapi.py +++ b/zun/compute/rpcapi.py @@ -56,9 +56,11 @@ class API(rpc_service.API): transport, context, topic=zun.conf.CONF.compute.topic) def container_create(self, context, host, container, limits, - requested_networks, run): + requested_networks, requested_volumes, run): self._cast(host, 'container_create', limits=limits, - requested_networks=requested_networks, container=container, + requested_networks=requested_networks, + requested_volumes=requested_volumes, + container=container, run=run) @check_container_host diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index ad996641a..5c1dde9aa 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -31,6 +31,7 @@ from zun.container.docker import utils as docker_utils from zun.container import driver from zun.network import network as zun_network from zun import objects +from zun.volume import driver as vol_driver CONF = zun.conf.CONF @@ -112,7 +113,8 @@ class DockerDriver(driver.ContainerDriver): except Exception: LOG.warning("Unable to read image data from tarfile") - def create(self, context, container, image, requested_networks): + def create(self, context, container, image, requested_networks, + requested_volumes): sandbox_id = container.get_sandbox_id() with docker_utils.docker_client() as docker: @@ -121,6 +123,7 @@ class DockerDriver(driver.ContainerDriver): LOG.debug('Creating container with image %(image)s name %(name)s', {'image': image['image'], 'name': name}) self._provision_network(context, network_api, requested_networks) + binds = self._get_binds(context, requested_volumes) kwargs = { 'name': self.get_container_name(container), 'command': container.command, @@ -147,6 +150,9 @@ class DockerDriver(driver.ContainerDriver): # host_config['pid_mode'] = 'container:%s' % sandbox_id host_config['ipc_mode'] = 'container:%s' % sandbox_id host_config['volumes_from'] = sandbox_id + else: + host_config['binds'] = binds + kwargs['volumes'] = [b['bind'] for b in binds.values()] if container.auto_remove: host_config['auto_remove'] = container.auto_remove if container.memory is not None: @@ -179,6 +185,15 @@ class DockerDriver(driver.ContainerDriver): self._get_or_create_docker_network( context, network_api, rq_network['network']) + def _get_binds(self, context, requested_volumes): + binds = {} + for volume in requested_volumes: + volume_driver = vol_driver.driver(provider=volume.volume_provider, + context=context) + source, destination = volume_driver.bind_mount(volume) + binds[source] = {'bind': destination} + return binds + def _setup_network_for_container(self, context, container, requested_networks, network_api): security_group_ids = utils.get_security_group_ids(context, container. @@ -675,14 +690,19 @@ class DockerDriver(driver.ContainerDriver): return value.encode('utf-8') def create_sandbox(self, context, container, requested_networks, + requested_volumes, image='kubernetes/pause'): with docker_utils.docker_client() as docker: network_api = zun_network.api(context=context, docker_api=docker) self._provision_network(context, network_api, requested_networks) + binds = self._get_binds(context, requested_volumes) + host_config = docker.create_host_config(binds=binds) name = self.get_sandbox_name(container) - sandbox = docker.create_container( - image, name=name, - hostname=container.hostname or name[:63]) + volumes = [b['bind'] for b in binds.values()] + sandbox = docker.create_container(image, name=name, + hostname=name[:63], + volumes=volumes, + host_config=host_config) container.set_sandbox_id(sandbox['Id']) addresses = self._setup_network_for_container( context, container, requested_networks, network_api) @@ -696,6 +716,18 @@ class DockerDriver(driver.ContainerDriver): docker.start(sandbox['Id']) return sandbox['Id'] + def attach_volume(self, context, volume_mapping): + volume_driver = vol_driver.driver( + provider=volume_mapping.volume_provider, + context=context) + volume_driver.attach(volume_mapping) + + def detach_volume(self, context, volume_mapping): + volume_driver = vol_driver.driver( + provider=volume_mapping.volume_provider, + context=context) + volume_driver.detach(volume_mapping) + def _get_or_create_docker_network(self, context, network_api, neutron_net_id): docker_net_name = self._get_docker_network_name(context, diff --git a/zun/container/driver.py b/zun/container/driver.py index 617c18d48..fa935842d 100644 --- a/zun/container/driver.py +++ b/zun/container/driver.py @@ -192,6 +192,12 @@ class ContainerDriver(object): def get_cpu_used(self): raise NotImplementedError() + def attach_volume(self, context, volume_mapping): + raise NotImplementedError() + + def detach_volume(self, context, volume_mapping): + raise NotImplementedError() + def add_security_group(self, context, container, security_group, **kwargs): raise NotImplementedError() diff --git a/zun/tests/base.py b/zun/tests/base.py index f8458ba85..16a7bdf44 100644 --- a/zun/tests/base.py +++ b/zun/tests/base.py @@ -47,6 +47,10 @@ class BaseTestCase(testscenarios.WithScenarios, base.BaseTestCase): self.addCleanup(CONF.reset) +class TestingException(Exception): + pass + + class TestCase(base.BaseTestCase): """Test case base class for all unit tests.""" diff --git a/zun/tests/unit/api/base.py b/zun/tests/unit/api/base.py index 978c0ba3c..6ed6d3ae6 100644 --- a/zun/tests/unit/api/base.py +++ b/zun/tests/unit/api/base.py @@ -27,7 +27,7 @@ from zun.tests.unit.db import base PATH_PREFIX = '/v1' -CURRENT_VERSION = "container 1.9" +CURRENT_VERSION = "container 1.11" class FunctionalTest(base.DbTestCase): diff --git a/zun/tests/unit/api/controllers/test_root.py b/zun/tests/unit/api/controllers/test_root.py index c0a58f291..731dd86a4 100644 --- a/zun/tests/unit/api/controllers/test_root.py +++ b/zun/tests/unit/api/controllers/test_root.py @@ -28,7 +28,7 @@ class TestRootController(api_base.FunctionalTest): 'default_version': {'id': 'v1', 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], - 'max_version': '1.10', + 'max_version': '1.11', 'min_version': '1.1', 'status': 'CURRENT'}, 'description': 'Zun is an OpenStack project which ' @@ -37,7 +37,7 @@ class TestRootController(api_base.FunctionalTest): 'versions': [{'id': 'v1', 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], - 'max_version': '1.10', + 'max_version': '1.11', 'min_version': '1.1', 'status': 'CURRENT'}]} diff --git a/zun/tests/unit/api/controllers/v1/test_containers.py b/zun/tests/unit/api/controllers/v1/test_containers.py index 4fb11d3e7..755baca1a 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -721,6 +721,53 @@ class TestContainerController(api_base.FunctionalTest): mock_authorize.return_value = fake_admin_authorize self.assertEqual(202, response.status_int) + @patch('zun.network.neutron.NeutronAPI.get_available_network') + @patch('zun.compute.api.API.container_show') + @patch('zun.compute.api.API.container_create') + @patch('zun.common.context.RequestContext.can') + @patch('zun.volume.cinder_api.CinderAPI.search_volume') + @patch('zun.volume.cinder_api.CinderAPI.ensure_volume_usable') + @patch('zun.compute.api.API.image_search') + def test_create_container_with_volume( + self, mock_search, mock_ensure_volume_usable, mock_search_volume, + mock_authorize, mock_container_create, mock_container_show, + mock_neutron_get_network): + fake_network = {'id': 'foo'} + mock_neutron_get_network.return_value = fake_network + fake_volume_id = 'fakevolid' + fake_volume = mock.Mock(id=fake_volume_id) + mock_search_volume.return_value = fake_volume + # Create a container with a command + params = ('{"name": "MyDocker", "image": "ubuntu",' + '"command": "env", "memory": "512",' + '"mounts": [{"source": "s", "destination": "d"}]}') + response = self.post('/v1/containers/', + params=params, + content_type='application/json') + self.assertEqual(202, response.status_int) + # get all containers + container = objects.Container.list(self.context)[0] + container.status = 'Stopped' + mock_container_show.return_value = container + response = self.app.get('/v1/containers/') + self.assertEqual(200, response.status_int) + self.assertEqual(2, len(response.json)) + c = response.json['containers'][0] + self.assertIsNotNone(c.get('uuid')) + self.assertEqual('MyDocker', c.get('name')) + self.assertEqual('env', c.get('command')) + self.assertEqual('Stopped', c.get('status')) + self.assertEqual('512M', c.get('memory')) + requested_networks = \ + mock_container_create.call_args[1]['requested_networks'] + self.assertEqual(1, len(requested_networks)) + self.assertEqual(fake_network['id'], requested_networks[0]['network']) + mock_search_volume.assert_called_once() + requested_volumes = \ + mock_container_create.call_args[1]['requested_volumes'] + self.assertEqual(1, len(requested_volumes)) + self.assertEqual(fake_volume_id, requested_volumes[0].volume_id) + @patch('zun.network.neutron.NeutronAPI.get_available_network') @patch('zun.compute.api.API.container_show') @patch('zun.compute.api.API.container_create') diff --git a/zun/tests/unit/compute/test_compute_manager.py b/zun/tests/unit/compute/test_compute_manager.py index da93a567e..750e5fb5b 100644 --- a/zun/tests/unit/compute/test_compute_manager.py +++ b/zun/tests/unit/compute/test_compute_manager.py @@ -24,6 +24,7 @@ from zun.compute import manager import zun.conf from zun.objects.container import Container from zun.objects.image import Image +from zun.objects.volume_mapping import VolumeMapping from zun.tests import base from zun.tests.unit.container.fake_driver import FakeDriver as fake_driver from zun.tests.unit.db import utils @@ -35,6 +36,27 @@ class FakeResourceTracker(object): return claims.NopClaim() +class FakeVolumeMapping(object): + + volume_provider = 'fake_provider' + container_path = 'fake_path' + container_uuid = 'fake-cid' + volume_id = 'fake-vid' + + def __init__(self): + self.__class__.volumes = [] + + def create(self, context): + self.__class__.volumes.append(self) + + def destroy(self): + self.__class__.volumes.remove(self) + + @classmethod + def list_by_container(cls, context, container_id): + return cls.volumes + + class TestManager(base.TestCase): def setUp(self): @@ -62,13 +84,14 @@ class TestManager(base.TestCase): mock_pull.return_value = image, False self.compute_manager._resource_tracker = FakeResourceTracker() networks = [] + volumes = [] self.compute_manager._do_container_create(self.context, container, - networks) + networks, volumes) mock_save.assert_called_with(self.context) mock_pull.assert_any_call(self.context, container.image, 'latest', 'always', 'glance') mock_create.assert_called_once_with(self.context, container, image, - networks) + networks, volumes) @mock.patch.object(Container, 'save') @mock.patch('zun.image.driver.pull_image') @@ -78,8 +101,9 @@ class TestManager(base.TestCase): container = Container(self.context, **utils.get_test_container()) mock_pull.side_effect = exception.DockerError("Pull Failed") networks = [] + volumes = [] self.compute_manager._do_container_create(self.context, container, - networks) + networks, volumes) mock_fail.assert_called_once_with(self.context, container, "Pull Failed") @@ -91,8 +115,9 @@ class TestManager(base.TestCase): container = Container(self.context, **utils.get_test_container()) mock_pull.side_effect = exception.ImageNotFound("Image Not Found") networks = [] + volumes = [] self.compute_manager._do_container_create(self.context, container, - networks) + networks, volumes) mock_fail.assert_called_once_with(self.context, container, "Image Not Found") @@ -105,8 +130,9 @@ class TestManager(base.TestCase): mock_pull.side_effect = exception.ZunException( message="Image Not Found") networks = [] + volumes = [] self.compute_manager._do_container_create(self.context, container, - networks) + networks, volumes) mock_fail.assert_called_once_with(self.context, container, "Image Not Found") @@ -124,18 +150,25 @@ class TestManager(base.TestCase): mock_create.side_effect = exception.DockerError("Creation Failed") self.compute_manager._resource_tracker = FakeResourceTracker() networks = [] + volumes = [] self.compute_manager._do_container_create(self.context, container, - networks) + networks, volumes) mock_fail.assert_called_once_with( self.context, container, "Creation Failed", unset_host=True) @mock.patch('zun.common.utils.spawn_n') @mock.patch.object(Container, 'save') + @mock.patch.object(VolumeMapping, 'list_by_container', + side_effect=FakeVolumeMapping.list_by_container) @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(fake_driver, 'detach_volume') + @mock.patch.object(fake_driver, 'attach_volume') @mock.patch.object(fake_driver, 'create') @mock.patch.object(fake_driver, 'start') - def test_container_run(self, mock_start, - mock_create, mock_pull, mock_save, mock_spawn_n): + def test_container_run( + self, mock_start, mock_create, mock_attach_volume, + mock_detach_volume, mock_pull, mock_list_by_container, mock_save, + mock_spawn_n): container = Container(self.context, **utils.get_test_container()) image = {'image': 'repo', 'path': 'out_path', 'driver': 'glance'} mock_create.return_value = container @@ -144,25 +177,74 @@ class TestManager(base.TestCase): container.status = 'Stopped' self.compute_manager._resource_tracker = FakeResourceTracker() networks = [] + volumes = [FakeVolumeMapping()] self.compute_manager.container_create( self.context, requested_networks=networks, + requested_volumes=volumes, container=container, limits=None, run=True) mock_save.assert_called_with(self.context) mock_pull.assert_any_call(self.context, container.image, 'latest', 'always', 'glance') mock_create.assert_called_once_with(self.context, container, image, - networks) + networks, volumes) mock_start.assert_called_once_with(self.context, container) + mock_attach_volume.assert_called_once() + mock_detach_volume.assert_not_called() + self.assertEqual(1, len(FakeVolumeMapping.volumes)) @mock.patch('zun.common.utils.spawn_n') @mock.patch.object(Container, 'save') + @mock.patch.object(VolumeMapping, 'list_by_container', + side_effect=FakeVolumeMapping.list_by_container) @mock.patch('zun.image.driver.pull_image') - @mock.patch.object(manager.Manager, '_fail_container') - def test_container_run_image_not_found(self, mock_fail, - mock_pull, mock_save, - mock_spawn_n): + @mock.patch.object(fake_driver, 'detach_volume') + @mock.patch.object(fake_driver, 'attach_volume') + @mock.patch.object(fake_driver, 'create') + @mock.patch.object(fake_driver, 'start') + def test_container_run_driver_attach_failed( + self, mock_start, mock_create, mock_attach_volume, + mock_detach_volume, mock_pull, mock_list_by_container, mock_save, + mock_spawn_n): + mock_attach_volume.side_effect = [None, base.TestingException("fake")] + container = Container(self.context, **utils.get_test_container()) + vol = FakeVolumeMapping() + vol2 = FakeVolumeMapping() + image = {'image': 'repo', 'path': 'out_path', 'driver': 'glance'} + mock_create.return_value = container + mock_pull.return_value = image, False + mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y) + container.status = 'Stopped' + self.compute_manager._resource_tracker = FakeResourceTracker() + networks = [] + volumes = [vol, vol2] + self.compute_manager.container_create( + self.context, + requested_networks=networks, + requested_volumes=volumes, + container=container, + limits=None, run=True) + mock_save.assert_called_with(self.context) + mock_pull.assert_not_called() + mock_create.assert_not_called() + mock_start.assert_not_called() + mock_attach_volume.assert_has_calls([ + mock.call(mock.ANY, vol), mock.call(mock.ANY, vol2)]) + mock_detach_volume.assert_has_calls([ + mock.call(mock.ANY, vol)]) + self.assertEqual(0, len(FakeVolumeMapping.volumes)) + + @mock.patch('zun.common.utils.spawn_n') + @mock.patch.object(Container, 'save') + @mock.patch.object(VolumeMapping, 'list_by_container', + side_effect=FakeVolumeMapping.list_by_container) + @mock.patch.object(fake_driver, 'detach_volume') + @mock.patch.object(fake_driver, 'attach_volume') + @mock.patch('zun.image.driver.pull_image') + def test_container_run_image_not_found( + self, mock_pull, mock_attach_volume, mock_detach_volume, + mock_list_by_container, mock_save, mock_spawn_n): container_dict = utils.get_test_container( image='test:latest', image_driver='docker', image_pull_policy='ifnotpresent') @@ -171,24 +253,32 @@ class TestManager(base.TestCase): message="Image Not Found") mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y) networks = [] + volumes = [FakeVolumeMapping()] self.compute_manager.container_create( self.context, requested_networks=networks, + requested_volumes=volumes, container=container, limits=None, run=True) mock_save.assert_called_with(self.context) - mock_fail.assert_called_with(self.context, - container, 'Image Not Found') + self.assertEqual('Error', container.status) + self.assertEqual('Image Not Found', container.status_reason) mock_pull.assert_called_once_with(self.context, 'test', 'latest', 'ifnotpresent', 'docker') + mock_attach_volume.assert_called_once() + mock_detach_volume.assert_called_once() + self.assertEqual(0, len(FakeVolumeMapping.volumes)) @mock.patch('zun.common.utils.spawn_n') @mock.patch.object(Container, 'save') + @mock.patch.object(VolumeMapping, 'list_by_container', + side_effect=FakeVolumeMapping.list_by_container) + @mock.patch.object(fake_driver, 'detach_volume') + @mock.patch.object(fake_driver, 'attach_volume') @mock.patch('zun.image.driver.pull_image') - @mock.patch.object(manager.Manager, '_fail_container') - def test_container_run_image_pull_exception_raised(self, mock_fail, - mock_pull, mock_save, - mock_spawn_n): + def test_container_run_image_pull_exception_raised( + self, mock_pull, mock_attach_volume, mock_detach_volume, + mock_list_by_container, mock_save, mock_spawn_n): container_dict = utils.get_test_container( image='test:latest', image_driver='docker', image_pull_policy='ifnotpresent') @@ -197,24 +287,32 @@ class TestManager(base.TestCase): message="Image Not Found") mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y) networks = [] + volumes = [FakeVolumeMapping()] self.compute_manager.container_create( self.context, requested_networks=networks, + requested_volumes=volumes, container=container, limits=None, run=True) mock_save.assert_called_with(self.context) - mock_fail.assert_called_with(self.context, - container, 'Image Not Found') + self.assertEqual('Error', container.status) + self.assertEqual('Image Not Found', container.status_reason) mock_pull.assert_called_once_with(self.context, 'test', 'latest', 'ifnotpresent', 'docker') + mock_attach_volume.assert_called_once() + mock_detach_volume.assert_called_once() + self.assertEqual(0, len(FakeVolumeMapping.volumes)) @mock.patch('zun.common.utils.spawn_n') @mock.patch.object(Container, 'save') + @mock.patch.object(VolumeMapping, 'list_by_container', + side_effect=FakeVolumeMapping.list_by_container) + @mock.patch.object(fake_driver, 'detach_volume') + @mock.patch.object(fake_driver, 'attach_volume') @mock.patch('zun.image.driver.pull_image') - @mock.patch.object(manager.Manager, '_fail_container') - def test_container_run_image_pull_docker_error(self, mock_fail, - mock_pull, mock_save, - mock_spawn_n): + def test_container_run_image_pull_docker_error( + self, mock_pull, mock_attach_volume, mock_detach_volume, + mock_list_by_container, mock_save, mock_spawn_n): container_dict = utils.get_test_container( image='test:latest', image_driver='docker', image_pull_policy='ifnotpresent') @@ -223,26 +321,34 @@ class TestManager(base.TestCase): message="Docker Error occurred") mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y) networks = [] + volumes = [FakeVolumeMapping()] self.compute_manager.container_create( self.context, requested_networks=networks, + requested_volumes=volumes, container=container, limits=None, run=True) mock_save.assert_called_with(self.context) - mock_fail.assert_called_with(self.context, - container, 'Docker Error occurred') + self.assertEqual('Error', container.status) + self.assertEqual('Docker Error occurred', container.status_reason) mock_pull.assert_called_once_with(self.context, 'test', 'latest', 'ifnotpresent', 'docker') + mock_attach_volume.assert_called_once() + mock_detach_volume.assert_called_once() + self.assertEqual(0, len(FakeVolumeMapping.volumes)) @mock.patch('zun.common.utils.spawn_n') @mock.patch.object(Container, 'save') + @mock.patch.object(VolumeMapping, 'list_by_container', + side_effect=FakeVolumeMapping.list_by_container) + @mock.patch.object(fake_driver, 'detach_volume') + @mock.patch.object(fake_driver, 'attach_volume') @mock.patch('zun.image.driver.pull_image') - @mock.patch.object(manager.Manager, '_fail_container') @mock.patch.object(fake_driver, 'create') - def test_container_run_create_raises_docker_error(self, mock_create, - mock_fail, - mock_pull, mock_save, - mock_spawn_n): + def test_container_run_create_raises_docker_error( + self, mock_create, mock_pull, mock_attach_volume, + mock_detach_volume, mock_list_by_container, mock_save, + mock_spawn_n): container = Container(self.context, **utils.get_test_container()) image = {'image': 'repo', 'path': 'out_path', 'driver': 'glance', 'repo': 'test', 'tag': 'testtag'} @@ -252,26 +358,34 @@ class TestManager(base.TestCase): mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y) self.compute_manager._resource_tracker = FakeResourceTracker() networks = [] + volumes = [FakeVolumeMapping()] self.compute_manager.container_create( self.context, requested_networks=networks, + requested_volumes=volumes, container=container, limits=None, run=True) mock_save.assert_called_with(self.context) - mock_fail.assert_called_with( - self.context, container, 'Docker Error occurred', unset_host=True) + self.assertEqual('Error', container.status) + self.assertEqual('Docker Error occurred', container.status_reason) mock_pull.assert_any_call(self.context, container.image, 'latest', 'always', 'glance') mock_create.assert_called_once_with( - self.context, container, image, networks) + self.context, container, image, networks, volumes) + mock_attach_volume.assert_called_once() + mock_detach_volume.assert_called_once() + self.assertEqual(0, len(FakeVolumeMapping.volumes)) @mock.patch.object(compute_node_tracker.ComputeNodeTracker, 'remove_usage_from_container') @mock.patch.object(Container, 'destroy') @mock.patch.object(Container, 'save') + @mock.patch.object(VolumeMapping, 'list_by_container') @mock.patch.object(fake_driver, 'delete') - def test_container_delete(self, mock_delete, mock_save, mock_cnt_destroy, - mock_remove_usage): + def test_container_delete( + self, mock_delete, mock_list_by_container, mock_save, + mock_cnt_destroy, mock_remove_usage): + mock_list_by_container.return_value = [] container = Container(self.context, **utils.get_test_container()) self.compute_manager._do_container_delete(self. context, container, False) @@ -307,10 +421,14 @@ class TestManager(base.TestCase): @mock.patch.object(Container, 'destroy') @mock.patch.object(manager.Manager, '_fail_container') @mock.patch.object(Container, 'save') + @mock.patch.object(VolumeMapping, 'list_by_container') @mock.patch.object(fake_driver, 'delete') - def test_container_delete_failed_force(self, mock_delete, mock_save, + def test_container_delete_failed_force(self, mock_delete, + mock_list_by_container, + mock_save, mock_fail, mock_destroy, mock_remove_usage): + mock_list_by_container.return_value = [] container = Container(self.context, **utils.get_test_container()) mock_delete.side_effect = exception.DockerError( message="Docker Error occurred") @@ -354,12 +472,15 @@ class TestManager(base.TestCase): @mock.patch.object(manager.Manager, '_fail_container') @mock.patch.object(manager.Manager, '_delete_sandbox') @mock.patch.object(Container, 'save') + @mock.patch.object(VolumeMapping, 'list_by_container') @mock.patch.object(fake_driver, 'delete') def test_container_delete_sandbox_failed_force(self, mock_delete, + mock_list_by_container, mock_save, mock_delete_sandbox, mock_fail, mock_destroy, mock_remove_usage): + mock_list_by_container.return_value = [] self.compute_manager.use_sandbox = True container = Container(self.context, **utils.get_test_container()) container.set_sandbox_id("sandbox_id") diff --git a/zun/tests/unit/container/docker/test_docker_driver.py b/zun/tests/unit/container/docker/test_docker_driver.py index ca19397ea..95b2a604d 100644 --- a/zun/tests/unit/container/docker/test_docker_driver.py +++ b/zun/tests/unit/container/docker/test_docker_driver.py @@ -91,14 +91,17 @@ class TestDockerDriver(base.DriverTestCase): return_value={'Id': 'val1', 'key1': 'val2'}) image = {'path': '', 'image': '', 'repo': '', 'tag': ''} mock_container = self.mock_default_container + networks = [] + volumes = [] result_container = self.driver.create(self.context, mock_container, - image, []) + 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'] = {} self.mock_docker.create_host_config.assert_called_once_with( **host_config) @@ -112,6 +115,7 @@ class TestDockerDriver(base.DriverTestCase): 'stdin_open': True, 'tty': True, 'hostname': 'testhost', + 'volumes': [], } self.mock_docker.create_container.assert_called_once_with( image['repo'] + ":" + image['tag'], **kwargs) @@ -364,15 +368,19 @@ class TestDockerDriver(base.DriverTestCase): mock_get_sandbox_name.return_value = sandbox_name self.mock_docker.create_container = mock.Mock( return_value={'Id': 'val1', 'key1': 'val2'}) + fake_host_config = mock.Mock() + self.mock_docker.create_host_config.return_value = fake_host_config mock_container = mock.MagicMock() hostname = 'my_hostname' mock_container.hostname = hostname requested_networks = [] + requested_volumes = [] result_sandbox_id = self.driver.create_sandbox( self.context, mock_container, requested_networks, - 'kubernetes/pause') + requested_volumes, 'kubernetes/pause') self.mock_docker.create_container.assert_called_once_with( - 'kubernetes/pause', name=sandbox_name, hostname=hostname) + 'kubernetes/pause', name=sandbox_name, hostname=sandbox_name, + host_config=fake_host_config, volumes=[]) self.assertEqual(result_sandbox_id, 'val1') @mock.patch('zun.network.kuryr_network.KuryrNetwork' @@ -386,14 +394,18 @@ class TestDockerDriver(base.DriverTestCase): mock_get_sandbox_name.return_value = sandbox_name self.mock_docker.create_container = mock.Mock( return_value={'Id': 'val1', 'key1': 'val2'}) + fake_host_config = mock.Mock() + self.mock_docker.create_host_config.return_value = fake_host_config mock_container = mock.MagicMock() mock_container.hostname = None requested_networks = [] + requested_volumes = [] result_sandbox_id = self.driver.create_sandbox( self.context, mock_container, requested_networks, - 'kubernetes/pause') + requested_volumes, 'kubernetes/pause') self.mock_docker.create_container.assert_called_once_with( - 'kubernetes/pause', name=sandbox_name, hostname=sandbox_name[:63]) + 'kubernetes/pause', name=sandbox_name, hostname=sandbox_name[:63], + host_config=fake_host_config, volumes=[]) self.assertEqual(result_sandbox_id, 'val1') def test_delete_sandbox(self):