From 21ae28cc1a101a8585028fce21b9507881e61c5b Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Sun, 8 Jul 2018 21:35:08 +0000 Subject: [PATCH] Support file injection on container Add support for file injection on creating the container. API users need to pass the file contents by using the 'mounts' attribute. The content will persist into a temp file in the compute host and bind-mount into the container. To achieve this, we introduce the volume driver called 'Local'. This driver will implement the volume driver interface and handle file injection related operations. In data model, we adds a new field 'contents' into the volume_mapping table. We also change 'volume_id' to be nullable because file injection doesn't need a cinder volume. A future work is to add the ability to limit the size and number of injected files. The limits should be configurable either via admin APIs or config options. Implements: blueprint inject-files-to-container Change-Id: I4ab6f50684f77bd7762e872d884ce11a7b0807ba --- api-ref/source/parameters.yaml | 6 ++ setup.cfg | 1 + zun/api/controllers/v1/containers.py | 46 ++++++++----- .../controllers/v1/schemas/parameter_types.py | 3 + zun/api/controllers/versions.py | 3 +- zun/api/rest_api_version_history.rst | 5 ++ zun/conf/volume.py | 12 ++++ zun/container/docker/driver.py | 30 ++++++-- ...4a_add_contents_to_volume_mapping_table.py | 40 +++++++++++ zun/db/sqlalchemy/models.py | 3 +- zun/objects/volume_mapping.py | 6 +- zun/tests/unit/api/base.py | 2 +- zun/tests/unit/api/controllers/test_root.py | 4 +- .../api/controllers/v1/test_containers.py | 49 +++++++++++++ zun/tests/unit/db/utils.py | 1 + zun/tests/unit/objects/test_objects.py | 2 +- zun/tests/unit/volume/test_driver.py | 69 +++++++++++++++++-- zun/volume/driver.py | 48 +++++++++++-- 18 files changed, 284 insertions(+), 46 deletions(-) create mode 100644 zun/db/sqlalchemy/alembic/versions/a9c9fb54274a_add_contents_to_volume_mapping_table.py diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 9fbd427b5..e0809b7ce 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -741,10 +741,16 @@ mounts: description: | A list of dictionary data to specify how volumes are mounted into the container. The container will mount the volumes at create time. + Each item can have an ``type`` attribute that specifies the volume + type. The supported volume types are ``volume`` or ``bind``. If this + attribute is not specified, the default is ``volume``. To provision a container with pre-existing Cinder volumes bind-mounted, specify the UUID or name of the volume in the ``source`` attribute. Alternatively, Cinder volumes can be dynamically created. In this case, the size of the volume needs to be specified in the ``size`` attribute. + Another option is to mount a user-provided file into the container. + In this case, the ``type`` attribute should be 'bind' and + the content of the file is contained in the ``source`` attribute. The volumes will be mounted into the file system of the container and the path to mount the volume needs to be specified in the ``destination`` attribute. diff --git a/setup.cfg b/setup.cfg index 53a6cb96e..e2200bf14 100644 --- a/setup.cfg +++ b/setup.cfg @@ -75,6 +75,7 @@ zun.network.driver = zun.volume.driver = cinder = zun.volume.driver:Cinder + local = zun.volume.driver:Local [extras] osprofiler = diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index c2dd7810c..b0b54c86f 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -506,26 +506,38 @@ class ContainersController(base.Controller): return phynet_name 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: - if mount.get('source'): - volume = cinder_api.search_volume(mount['source']) - auto_remove = False - else: - volume = cinder_api.create_volume(mount['size']) - auto_remove = True - 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, - auto_remove=auto_remove) + volume_dict = { + 'volume_id': None, + 'container_path': None, + 'auto_remove': False, + 'contents': None, + 'user_id': context.user_id, + 'project_id': context.project_id, + } + volume_type = mount.get('type', 'volume') + if volume_type == 'volume': + if mount.get('source'): + volume = cinder_api.search_volume(mount['source']) + cinder_api.ensure_volume_usable(volume) + volume_dict['volume_id'] = volume.id + volume_dict['container_path'] = mount['destination'] + volume_dict['volume_provider'] = 'cinder' + elif mount.get('size'): + volume = cinder_api.create_volume(mount['size']) + cinder_api.ensure_volume_usable(volume) + volume_dict['volume_id'] = volume.id + volume_dict['container_path'] = mount['destination'] + volume_dict['volume_provider'] = 'cinder' + volume_dict['auto_remove'] = True + elif volume_type == 'bind': + volume_dict['contents'] = mount.pop('source', '') + volume_dict['container_path'] = mount['destination'] + volume_dict['volume_provider'] = 'local' + + volmapp = objects.VolumeMapping(context, **volume_dict) requested_volumes.append(volmapp) return requested_volumes diff --git a/zun/api/controllers/v1/schemas/parameter_types.py b/zun/api/controllers/v1/schemas/parameter_types.py index 8d35c658e..e354f051b 100644 --- a/zun/api/controllers/v1/schemas/parameter_types.py +++ b/zun/api/controllers/v1/schemas/parameter_types.py @@ -201,6 +201,9 @@ mounts = { 'items': { 'type': 'object', 'properties': { + 'type': { + 'type': ['string'], + }, 'source': { 'type': ['string'], }, diff --git a/zun/api/controllers/versions.py b/zun/api/controllers/versions.py index e2b691afc..dfc4ec9fa 100644 --- a/zun/api/controllers/versions.py +++ b/zun/api/controllers/versions.py @@ -55,10 +55,11 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 1.20 - Convert type of 'command' from string to list * 1.21 - Add support privileged * 1.22 - Add healthcheck to container create + * 1.23 - Add attribute 'type' to parameter 'mounts' """ BASE_VER = '1.1' -CURRENT_MAX_VER = '1.22' +CURRENT_MAX_VER = '1.23' class Version(object): diff --git a/zun/api/rest_api_version_history.rst b/zun/api/rest_api_version_history.rst index 69216bdda..5c6d26aeb 100644 --- a/zun/api/rest_api_version_history.rst +++ b/zun/api/rest_api_version_history.rst @@ -183,3 +183,8 @@ user documentation. Add healthcheck to container create +1.23 +---- + + Add support for file injection when creating a container. + The content of the file is sent to Zun server via parameter 'mounts'. diff --git a/zun/conf/volume.py b/zun/conf/volume.py index 2cfd022f3..e099e8451 100644 --- a/zun/conf/volume.py +++ b/zun/conf/volume.py @@ -19,7 +19,19 @@ volume_group = cfg.OptGroup(name='volume', volume_opts = [ cfg.StrOpt('driver', default='cinder', + deprecated_for_removal=True, help='Defines which driver to use for container volume.'), + cfg.ListOpt('driver_list', + default=['cinder', 'local'], + help="""Defines the list of volume driver to use. +Possible values: +* ``cinder`` +* ``local`` +Services which consume this: +* ``zun-compute`` +Interdependencies to other options: +* None +"""), cfg.StrOpt('volume_dir', default='$state_path/mnt', help='At which the docker volume will create.'), diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index 9502efd8b..4a5e3e885 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -112,11 +112,14 @@ class DockerDriver(driver.ContainerDriver): super(DockerDriver, self).__init__() self._host = host.Host() self._get_host_storage_info() - self.volume_driver = vol_driver.driver() self.image_drivers = {} for driver_name in CONF.image_driver_list: driver = img_driver.load_image_driver(driver_name) self.image_drivers[driver_name] = driver + self.volume_drivers = {} + for driver_name in CONF.volume.driver_list: + driver = vol_driver.driver(driver_name) + self.volume_drivers[driver_name] = driver def _get_host_storage_info(self): storage_info = self._host.get_storage_info() @@ -380,8 +383,8 @@ class DockerDriver(driver.ContainerDriver): def _get_binds(self, context, requested_volumes): binds = {} for volume in requested_volumes: - source, destination = self.volume_driver.bind_mount(context, - volume) + volume_driver = self._get_volume_driver(volume) + source, destination = volume_driver.bind_mount(context, volume) binds[source] = {'bind': destination} return binds @@ -990,17 +993,30 @@ class DockerDriver(driver.ContainerDriver): docker.start(sandbox['Id']) return sandbox['Id'] + def _get_volume_driver(self, volume_mapping): + driver_name = volume_mapping.volume_provider + driver = self.volume_drivers.get(driver_name) + if not driver: + msg = _("The volume provider '%s' is not supported") % driver_name + raise exception.ZunException(msg) + + return driver + def attach_volume(self, context, volume_mapping): - self.volume_driver.attach(context, volume_mapping) + volume_driver = self._get_volume_driver(volume_mapping) + volume_driver.attach(context, volume_mapping) def detach_volume(self, context, volume_mapping): - self.volume_driver.detach(context, volume_mapping) + volume_driver = self._get_volume_driver(volume_mapping) + volume_driver.detach(context, volume_mapping) def delete_volume(self, context, volume_mapping): - self.volume_driver.delete(context, volume_mapping) + volume_driver = self._get_volume_driver(volume_mapping) + volume_driver.delete(context, volume_mapping) def get_volume_status(self, context, volume_mapping): - return self.volume_driver.get_volume_status(context, volume_mapping) + volume_driver = self._get_volume_driver(volume_mapping) + return volume_driver.get_volume_status(context, volume_mapping) def check_multiattach(self, context, volume_mapping): return self.volume_driver.check_multiattach(context, volume_mapping) diff --git a/zun/db/sqlalchemy/alembic/versions/a9c9fb54274a_add_contents_to_volume_mapping_table.py b/zun/db/sqlalchemy/alembic/versions/a9c9fb54274a_add_contents_to_volume_mapping_table.py new file mode 100644 index 000000000..ee8adde69 --- /dev/null +++ b/zun/db/sqlalchemy/alembic/versions/a9c9fb54274a_add_contents_to_volume_mapping_table.py @@ -0,0 +1,40 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""add_contents_to_volume_mapping_table + +Revision ID: a9c9fb54274a +Revises: bc56b9932dd9 +Create Date: 2018-08-10 02:49:27.524151 + +""" + +# revision identifiers, used by Alembic. +revision = 'a9c9fb54274a' +down_revision = 'bc56b9932dd9' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def MediumText(): + return sa.Text().with_variant(sa.dialects.mysql.MEDIUMTEXT(), 'mysql') + + +def upgrade(): + op.add_column('volume_mapping', + sa.Column('contents', MediumText(), nullable=True)) + op.alter_column('volume_mapping', 'volume_id', + existing_type=sa.String(36), + nullable=True) diff --git a/zun/db/sqlalchemy/models.py b/zun/db/sqlalchemy/models.py index ac16bc090..f7d6ca99b 100644 --- a/zun/db/sqlalchemy/models.py +++ b/zun/db/sqlalchemy/models.py @@ -186,11 +186,12 @@ class VolumeMapping(Base): id = Column(Integer, primary_key=True, nullable=False) project_id = Column(String(255), nullable=True) user_id = Column(String(255), nullable=True) - volume_id = Column(String(36), nullable=False) + volume_id = Column(String(36), nullable=True) volume_provider = Column(String(36), nullable=False) container_path = Column(String(255), nullable=True) container_uuid = Column(String(36), ForeignKey('container.uuid')) connection_info = Column(MediumText()) + contents = Column(MediumText()) container = orm.relationship( Container, backref=orm.backref('volume'), diff --git a/zun/objects/volume_mapping.py b/zun/objects/volume_mapping.py index 677c960d8..f8ae77cf5 100644 --- a/zun/objects/volume_mapping.py +++ b/zun/objects/volume_mapping.py @@ -36,14 +36,15 @@ class VolumeMapping(base.ZunPersistentObject, base.ZunObject): # Version 1.0: Initial version # Version 1.1: Add field "auto_remove" # Version 1.2: Add field "host" - VERSION = '1.2' + # Version 1.3: Add field "contents" + VERSION = '1.3' fields = { 'id': fields.IntegerField(), 'uuid': fields.UUIDField(nullable=False), 'project_id': fields.StringField(nullable=True), 'user_id': fields.StringField(nullable=True), - 'volume_id': fields.UUIDField(nullable=False), + 'volume_id': fields.UUIDField(nullable=True), 'volume_provider': fields.StringField(nullable=False), 'container_path': fields.StringField(nullable=True), 'container_uuid': fields.UUIDField(nullable=True), @@ -51,6 +52,7 @@ class VolumeMapping(base.ZunPersistentObject, base.ZunObject): 'connection_info': fields.SensitiveStringField(nullable=True), 'auto_remove': fields.BooleanField(nullable=True), 'host': fields.StringField(nullable=True), + 'contents': fields.SensitiveStringField(nullable=True), } @staticmethod diff --git a/zun/tests/unit/api/base.py b/zun/tests/unit/api/base.py index 5c698ec71..0cb9eb0b2 100644 --- a/zun/tests/unit/api/base.py +++ b/zun/tests/unit/api/base.py @@ -26,7 +26,7 @@ from zun.tests.unit.db import base PATH_PREFIX = '/v1' -CURRENT_VERSION = "container 1.22" +CURRENT_VERSION = "container 1.23" 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 b7c3fecca..a31eb1a44 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.22', + 'max_version': '1.23', '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.22', + 'max_version': '1.23', '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 454692592..717416d72 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -684,11 +684,60 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(1, len(requested_networks)) self.assertEqual(fake_network['id'], requested_networks[0]['network']) mock_create_volume.assert_called_once() + mock_ensure_volume_usable.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') + @patch('zun.common.context.RequestContext.can') + @patch('zun.volume.cinder_api.CinderAPI.create_volume') + @patch('zun.volume.cinder_api.CinderAPI.ensure_volume_usable') + @patch('zun.compute.api.API.image_search') + def test_create_container_with_injected_file( + self, mock_search, mock_ensure_volume_usable, mock_create_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 + # Create a container with a command + params = ('{"name": "MyDocker", "image": "ubuntu",' + '"command": ["env"], "memory": "512",' + '"mounts": [{"destination": "d", "source": "hello",' + ' "type": "bind"}]}') + 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 = 'Creating' + 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('Creating', c.get('status')) + self.assertEqual('512', c.get('memory')) + self.assertIn('host', c) + 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_create_volume.assert_not_called() + mock_ensure_volume_usable.assert_not_called() + requested_volumes = \ + mock_container_create.call_args[1]['requested_volumes'] + self.assertEqual(1, len(requested_volumes)) + self.assertIsNone(requested_volumes[0].volume_id) + self.assertEqual('local', requested_volumes[0].volume_provider) + @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/db/utils.py b/zun/tests/unit/db/utils.py index b78ce11d6..3faee6ad2 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -152,6 +152,7 @@ def get_test_volume_mapping(**kwargs): 'connection_info': kwargs.get('connection_info', 'fake_info'), 'auto_remove': kwargs.get('auto_remove', False), 'host': kwargs.get('host', 'fake_host'), + 'contents': kwargs.get('contents', 'fake-contents'), } diff --git a/zun/tests/unit/objects/test_objects.py b/zun/tests/unit/objects/test_objects.py index 652ee394f..0ffb91032 100644 --- a/zun/tests/unit/objects/test_objects.py +++ b/zun/tests/unit/objects/test_objects.py @@ -345,7 +345,7 @@ class TestObject(test_base.TestCase, _TestObject): # https://docs.openstack.org/zun/latest/ object_data = { 'Container': '1.36-ad2bacdaa51afd0047e96003f93ff181', - 'VolumeMapping': '1.2-2230102beda09cf5caabd130c600dc92', + 'VolumeMapping': '1.3-14e3f9fc64e7afd751727c6ad3f32a94', 'Image': '1.1-330e6205c80b99b59717e1cfc6a79935', 'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd', 'NUMANode': '1.0-cba878b70b2f8b52f1e031b41ac13b4e', diff --git a/zun/tests/unit/volume/test_driver.py b/zun/tests/unit/volume/test_driver.py index 5faf1a8ba..88fdd34f7 100644 --- a/zun/tests/unit/volume/test_driver.py +++ b/zun/tests/unit/volume/test_driver.py @@ -13,6 +13,7 @@ import mock from oslo_serialization import jsonutils +from oslo_utils import uuidutils from zun.common import exception import zun.conf @@ -23,10 +24,11 @@ from zun.volume import driver CONF = zun.conf.CONF -class VolumeDriverTestCase(base.TestCase): +class CinderVolumeDriverTestCase(base.TestCase): def setUp(self): - super(VolumeDriverTestCase, self).setUp() + super(CinderVolumeDriverTestCase, self).setUp() + self.fake_uuid = uuidutils.generate_uuid() self.fake_volume_id = 'fake-volume-id' self.fake_devpath = '/fake-path' self.fake_mountpoint = '/fake-mountpoint' @@ -35,6 +37,7 @@ class VolumeDriverTestCase(base.TestCase): 'data': {'device_path': self.fake_devpath}, } self.volume = mock.MagicMock() + self.volume.uuid = self.fake_uuid self.volume.volume_provider = 'cinder' self.volume.volume_id = self.fake_volume_id self.volume.container_path = self.fake_container_path @@ -55,7 +58,7 @@ class VolumeDriverTestCase(base.TestCase): volume_driver.attach(self.context, self.volume) mock_cinder_workflow.attach_volume.assert_called_once_with(self.volume) - mock_get_mountpoint.assert_called_once_with(self.fake_volume_id) + mock_get_mountpoint.assert_called_once_with(self.fake_uuid) mock_do_mount.assert_called_once_with( self.fake_devpath, self.fake_mountpoint, CONF.volume.fstype) mock_cinder_workflow.detach_volume.assert_not_called() @@ -112,7 +115,7 @@ class VolumeDriverTestCase(base.TestCase): volume_driver.attach, self.context, self.volume) mock_cinder_workflow.attach_volume.assert_called_once_with(self.volume) - mock_get_mountpoint.assert_called_once_with(self.fake_volume_id) + mock_get_mountpoint.assert_called_once_with(self.fake_uuid) mock_do_mount.assert_called_once_with( self.fake_devpath, self.fake_mountpoint, CONF.volume.fstype) mock_cinder_workflow.detach_volume.assert_called_once_with(self.volume) @@ -143,7 +146,7 @@ class VolumeDriverTestCase(base.TestCase): volume_driver.attach, self.context, self.volume) mock_cinder_workflow.attach_volume.assert_called_once_with(self.volume) - mock_get_mountpoint.assert_called_once_with(self.fake_volume_id) + mock_get_mountpoint.assert_called_once_with(self.fake_uuid) mock_do_mount.assert_called_once_with( self.fake_devpath, self.fake_mountpoint, CONF.volume.fstype) mock_cinder_workflow.detach_volume.assert_called_once_with(self.volume) @@ -162,7 +165,7 @@ class VolumeDriverTestCase(base.TestCase): volume_driver.detach(self.context, self.volume) mock_cinder_workflow.detach_volume.assert_called_once_with(self.volume) - mock_get_mountpoint.assert_called_once_with(self.fake_volume_id) + mock_get_mountpoint.assert_called_once_with(self.fake_uuid) mock_do_unmount.assert_called_once_with(self.fake_mountpoint) mock_rmtree.assert_called_once_with(self.fake_mountpoint) @@ -179,7 +182,7 @@ class VolumeDriverTestCase(base.TestCase): self.assertEqual(self.fake_mountpoint, source) self.assertEqual(self.fake_container_path, destination) - mock_get_mountpoint.assert_called_once_with(self.fake_volume_id) + mock_get_mountpoint.assert_called_once_with(self.fake_uuid) @mock.patch('shutil.rmtree') @mock.patch('zun.common.mount.get_mountpoint') @@ -197,3 +200,55 @@ class VolumeDriverTestCase(base.TestCase): mock_cinder_workflow.delete_volume.assert_called_once_with(self.volume) mock_rmtree.assert_called_once_with(self.fake_mountpoint) + + +class LocalVolumeDriverTestCase(base.TestCase): + + def setUp(self): + super(LocalVolumeDriverTestCase, self).setUp() + self.fake_uuid = uuidutils.generate_uuid() + self.fake_mountpoint = '/fake-mountpoint' + self.fake_container_path = '/fake-container-path' + self.fake_contents = 'fake-contents' + self.volume = mock.MagicMock() + self.volume.uuid = self.fake_uuid + self.volume.volume_provider = 'local' + self.volume.container_path = self.fake_container_path + self.volume.contents = self.fake_contents + + @mock.patch('oslo_utils.fileutils.ensure_tree') + @mock.patch('zun.common.mount.get_mountpoint') + def test_attach(self, mock_get_mountpoint, mock_ensure_tree): + mock_get_mountpoint.return_value = self.fake_mountpoint + volume_driver = driver.Local() + + with mock.patch('zun.volume.driver.open', mock.mock_open() + ) as mock_open: + volume_driver.attach(self.context, self.volume) + + expected_file_path = self.fake_mountpoint + '/' + self.fake_uuid + mock_open.assert_called_once_with(expected_file_path, 'wb') + mock_open().write.assert_called_once_with(self.fake_contents) + mock_get_mountpoint.assert_called_once_with(self.fake_uuid) + + @mock.patch('shutil.rmtree') + @mock.patch('zun.common.mount.get_mountpoint') + def test_detach(self, mock_get_mountpoint, mock_rmtree): + mock_get_mountpoint.return_value = self.fake_mountpoint + volume_driver = driver.Local() + volume_driver.detach(self.context, self.volume) + + mock_get_mountpoint.assert_called_once_with(self.fake_uuid) + mock_rmtree.assert_called_once_with(self.fake_mountpoint) + + @mock.patch('zun.common.mount.get_mountpoint') + def test_bind_mount(self, mock_get_mountpoint): + mock_get_mountpoint.return_value = self.fake_mountpoint + volume_driver = driver.Local() + source, destination = volume_driver.bind_mount( + self.context, self.volume) + + expected_file_path = self.fake_mountpoint + '/' + self.fake_uuid + self.assertEqual(expected_file_path, source) + self.assertEqual(self.fake_container_path, destination) + mock_get_mountpoint.assert_called_once_with(self.fake_uuid) diff --git a/zun/volume/driver.py b/zun/volume/driver.py index 4be343659..3b5e86cd6 100644 --- a/zun/volume/driver.py +++ b/zun/volume/driver.py @@ -33,12 +33,11 @@ LOG = logging.getLogger(__name__) CONF = zun.conf.CONF -def driver(*args, **kwargs): - name = CONF.volume.driver - LOG.info("Loading volume driver '%s'", name) +def driver(driver_name, *args, **kwargs): + LOG.info("Loading volume driver '%s'", driver_name) volume_driver = stevedore_driver.DriverManager( "zun.volume.driver", - name, + driver_name, invoke_on_load=True, invoke_args=args, invoke_kwds=kwargs).driver @@ -84,6 +83,41 @@ class VolumeDriver(object): raise NotImplementedError() +class Local(VolumeDriver): + + supported_providers = ['local'] + + @validate_volume_provider(supported_providers) + def attach(self, context, volume): + mountpoint = mount.get_mountpoint(volume.uuid) + fileutils.ensure_tree(mountpoint) + filename = '/'.join([mountpoint, volume.uuid]) + with open(filename, 'wb') as fd: + fd.write(volume.contents) + + def _remove_local_file(self, volume): + mountpoint = mount.get_mountpoint(volume.uuid) + shutil.rmtree(mountpoint) + + @validate_volume_provider(supported_providers) + def detach(self, context, volume): + self._remove_local_file(volume) + + @validate_volume_provider(supported_providers) + def delete(self, context, volume): + self._remove_local_file(volume) + + @validate_volume_provider(supported_providers) + def bind_mount(self, context, volume): + mountpoint = mount.get_mountpoint(volume.uuid) + filename = '/'.join([mountpoint, volume.uuid]) + return filename, volume.container_path + + @validate_volume_provider(supported_providers) + def get_volume_status(self, context, volume): + return 'available' + + class Cinder(VolumeDriver): supported_providers = [ @@ -105,7 +139,7 @@ class Cinder(VolumeDriver): LOG.exception("Failed to detach volume") def _mount_device(self, volume, devpath): - mountpoint = mount.get_mountpoint(volume.volume_id) + mountpoint = mount.get_mountpoint(volume.uuid) fileutils.ensure_tree(mountpoint) mount.do_mount(devpath, mountpoint, CONF.volume.fstype) @@ -123,13 +157,13 @@ class Cinder(VolumeDriver): def _unmount_device(self, volume): if hasattr(volume, 'connection_info'): - mountpoint = mount.get_mountpoint(volume.volume_id) + mountpoint = mount.get_mountpoint(volume.uuid) mount.do_unmount(mountpoint) shutil.rmtree(mountpoint) @validate_volume_provider(supported_providers) def bind_mount(self, context, volume): - mountpoint = mount.get_mountpoint(volume.volume_id) + mountpoint = mount.get_mountpoint(volume.uuid) return mountpoint, volume.container_path @validate_volume_provider(supported_providers)