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
This commit is contained in:
Hongbin Lu
2018-07-08 21:35:08 +00:00
parent a13a693313
commit 21ae28cc1a
18 changed files with 284 additions and 46 deletions

View File

@@ -741,10 +741,16 @@ mounts:
description: | description: |
A list of dictionary data to specify how volumes are mounted into the A list of dictionary data to specify how volumes are mounted into the
container. The container will mount the volumes at create time. 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, To provision a container with pre-existing Cinder volumes bind-mounted,
specify the UUID or name of the volume in the ``source`` attribute. specify the UUID or name of the volume in the ``source`` attribute.
Alternatively, Cinder volumes can be dynamically created. In this case, Alternatively, Cinder volumes can be dynamically created. In this case,
the size of the volume needs to be specified in the ``size`` attribute. 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 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`` the path to mount the volume needs to be specified in the ``destination``
attribute. attribute.

View File

@@ -75,6 +75,7 @@ zun.network.driver =
zun.volume.driver = zun.volume.driver =
cinder = zun.volume.driver:Cinder cinder = zun.volume.driver:Cinder
local = zun.volume.driver:Local
[extras] [extras]
osprofiler = osprofiler =

View File

@@ -506,26 +506,38 @@ class ContainersController(base.Controller):
return phynet_name return phynet_name
def _build_requested_volumes(self, context, mounts): 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) cinder_api = cinder.CinderAPI(context)
requested_volumes = [] requested_volumes = []
for mount in mounts: for mount in mounts:
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'): if mount.get('source'):
volume = cinder_api.search_volume(mount['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) cinder_api.ensure_volume_usable(volume)
volmapp = objects.VolumeMapping( volume_dict['volume_id'] = volume.id
context, volume_dict['container_path'] = mount['destination']
volume_id=volume.id, volume_provider='cinder', volume_dict['volume_provider'] = 'cinder'
container_path=mount['destination'], elif mount.get('size'):
user_id=context.user_id, volume = cinder_api.create_volume(mount['size'])
project_id=context.project_id, cinder_api.ensure_volume_usable(volume)
auto_remove=auto_remove) 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) requested_volumes.append(volmapp)
return requested_volumes return requested_volumes

View File

@@ -201,6 +201,9 @@ mounts = {
'items': { 'items': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'type': {
'type': ['string'],
},
'source': { 'source': {
'type': ['string'], 'type': ['string'],
}, },

View File

@@ -55,10 +55,11 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 1.20 - Convert type of 'command' from string to list * 1.20 - Convert type of 'command' from string to list
* 1.21 - Add support privileged * 1.21 - Add support privileged
* 1.22 - Add healthcheck to container create * 1.22 - Add healthcheck to container create
* 1.23 - Add attribute 'type' to parameter 'mounts'
""" """
BASE_VER = '1.1' BASE_VER = '1.1'
CURRENT_MAX_VER = '1.22' CURRENT_MAX_VER = '1.23'
class Version(object): class Version(object):

View File

@@ -183,3 +183,8 @@ user documentation.
Add healthcheck to container create 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'.

View File

@@ -19,7 +19,19 @@ volume_group = cfg.OptGroup(name='volume',
volume_opts = [ volume_opts = [
cfg.StrOpt('driver', cfg.StrOpt('driver',
default='cinder', default='cinder',
deprecated_for_removal=True,
help='Defines which driver to use for container volume.'), 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', cfg.StrOpt('volume_dir',
default='$state_path/mnt', default='$state_path/mnt',
help='At which the docker volume will create.'), help='At which the docker volume will create.'),

View File

@@ -112,11 +112,14 @@ class DockerDriver(driver.ContainerDriver):
super(DockerDriver, self).__init__() super(DockerDriver, self).__init__()
self._host = host.Host() self._host = host.Host()
self._get_host_storage_info() self._get_host_storage_info()
self.volume_driver = vol_driver.driver()
self.image_drivers = {} self.image_drivers = {}
for driver_name in CONF.image_driver_list: for driver_name in CONF.image_driver_list:
driver = img_driver.load_image_driver(driver_name) driver = img_driver.load_image_driver(driver_name)
self.image_drivers[driver_name] = driver 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): def _get_host_storage_info(self):
storage_info = self._host.get_storage_info() storage_info = self._host.get_storage_info()
@@ -380,8 +383,8 @@ class DockerDriver(driver.ContainerDriver):
def _get_binds(self, context, requested_volumes): def _get_binds(self, context, requested_volumes):
binds = {} binds = {}
for volume in requested_volumes: for volume in requested_volumes:
source, destination = self.volume_driver.bind_mount(context, volume_driver = self._get_volume_driver(volume)
volume) source, destination = volume_driver.bind_mount(context, volume)
binds[source] = {'bind': destination} binds[source] = {'bind': destination}
return binds return binds
@@ -990,17 +993,30 @@ class DockerDriver(driver.ContainerDriver):
docker.start(sandbox['Id']) docker.start(sandbox['Id'])
return 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): 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): 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): 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): 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): def check_multiattach(self, context, volume_mapping):
return self.volume_driver.check_multiattach(context, volume_mapping) return self.volume_driver.check_multiattach(context, volume_mapping)

View File

@@ -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)

View File

@@ -186,11 +186,12 @@ class VolumeMapping(Base):
id = Column(Integer, primary_key=True, nullable=False) id = Column(Integer, primary_key=True, nullable=False)
project_id = Column(String(255), nullable=True) project_id = Column(String(255), nullable=True)
user_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) volume_provider = Column(String(36), nullable=False)
container_path = Column(String(255), nullable=True) container_path = Column(String(255), nullable=True)
container_uuid = Column(String(36), ForeignKey('container.uuid')) container_uuid = Column(String(36), ForeignKey('container.uuid'))
connection_info = Column(MediumText()) connection_info = Column(MediumText())
contents = Column(MediumText())
container = orm.relationship( container = orm.relationship(
Container, Container,
backref=orm.backref('volume'), backref=orm.backref('volume'),

View File

@@ -36,14 +36,15 @@ class VolumeMapping(base.ZunPersistentObject, base.ZunObject):
# Version 1.0: Initial version # Version 1.0: Initial version
# Version 1.1: Add field "auto_remove" # Version 1.1: Add field "auto_remove"
# Version 1.2: Add field "host" # Version 1.2: Add field "host"
VERSION = '1.2' # Version 1.3: Add field "contents"
VERSION = '1.3'
fields = { fields = {
'id': fields.IntegerField(), 'id': fields.IntegerField(),
'uuid': fields.UUIDField(nullable=False), 'uuid': fields.UUIDField(nullable=False),
'project_id': fields.StringField(nullable=True), 'project_id': fields.StringField(nullable=True),
'user_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), 'volume_provider': fields.StringField(nullable=False),
'container_path': fields.StringField(nullable=True), 'container_path': fields.StringField(nullable=True),
'container_uuid': fields.UUIDField(nullable=True), 'container_uuid': fields.UUIDField(nullable=True),
@@ -51,6 +52,7 @@ class VolumeMapping(base.ZunPersistentObject, base.ZunObject):
'connection_info': fields.SensitiveStringField(nullable=True), 'connection_info': fields.SensitiveStringField(nullable=True),
'auto_remove': fields.BooleanField(nullable=True), 'auto_remove': fields.BooleanField(nullable=True),
'host': fields.StringField(nullable=True), 'host': fields.StringField(nullable=True),
'contents': fields.SensitiveStringField(nullable=True),
} }
@staticmethod @staticmethod

View File

@@ -26,7 +26,7 @@ from zun.tests.unit.db import base
PATH_PREFIX = '/v1' PATH_PREFIX = '/v1'
CURRENT_VERSION = "container 1.22" CURRENT_VERSION = "container 1.23"
class FunctionalTest(base.DbTestCase): class FunctionalTest(base.DbTestCase):

View File

@@ -28,7 +28,7 @@ class TestRootController(api_base.FunctionalTest):
'default_version': 'default_version':
{'id': 'v1', {'id': 'v1',
'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}],
'max_version': '1.22', 'max_version': '1.23',
'min_version': '1.1', 'min_version': '1.1',
'status': 'CURRENT'}, 'status': 'CURRENT'},
'description': 'Zun is an OpenStack project which ' 'description': 'Zun is an OpenStack project which '
@@ -37,7 +37,7 @@ class TestRootController(api_base.FunctionalTest):
'versions': [{'id': 'v1', 'versions': [{'id': 'v1',
'links': [{'href': 'http://localhost/v1/', 'links': [{'href': 'http://localhost/v1/',
'rel': 'self'}], 'rel': 'self'}],
'max_version': '1.22', 'max_version': '1.23',
'min_version': '1.1', 'min_version': '1.1',
'status': 'CURRENT'}]} 'status': 'CURRENT'}]}

View File

@@ -684,11 +684,60 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual(1, len(requested_networks)) self.assertEqual(1, len(requested_networks))
self.assertEqual(fake_network['id'], requested_networks[0]['network']) self.assertEqual(fake_network['id'], requested_networks[0]['network'])
mock_create_volume.assert_called_once() mock_create_volume.assert_called_once()
mock_ensure_volume_usable.assert_called_once()
requested_volumes = \ requested_volumes = \
mock_container_create.call_args[1]['requested_volumes'] mock_container_create.call_args[1]['requested_volumes']
self.assertEqual(1, len(requested_volumes)) self.assertEqual(1, len(requested_volumes))
self.assertEqual(fake_volume_id, requested_volumes[0].volume_id) 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.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_show') @patch('zun.compute.api.API.container_show')
@patch('zun.compute.api.API.container_create') @patch('zun.compute.api.API.container_create')

View File

@@ -152,6 +152,7 @@ def get_test_volume_mapping(**kwargs):
'connection_info': kwargs.get('connection_info', 'fake_info'), 'connection_info': kwargs.get('connection_info', 'fake_info'),
'auto_remove': kwargs.get('auto_remove', False), 'auto_remove': kwargs.get('auto_remove', False),
'host': kwargs.get('host', 'fake_host'), 'host': kwargs.get('host', 'fake_host'),
'contents': kwargs.get('contents', 'fake-contents'),
} }

View File

@@ -345,7 +345,7 @@ class TestObject(test_base.TestCase, _TestObject):
# https://docs.openstack.org/zun/latest/ # https://docs.openstack.org/zun/latest/
object_data = { object_data = {
'Container': '1.36-ad2bacdaa51afd0047e96003f93ff181', 'Container': '1.36-ad2bacdaa51afd0047e96003f93ff181',
'VolumeMapping': '1.2-2230102beda09cf5caabd130c600dc92', 'VolumeMapping': '1.3-14e3f9fc64e7afd751727c6ad3f32a94',
'Image': '1.1-330e6205c80b99b59717e1cfc6a79935', 'Image': '1.1-330e6205c80b99b59717e1cfc6a79935',
'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd', 'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd',
'NUMANode': '1.0-cba878b70b2f8b52f1e031b41ac13b4e', 'NUMANode': '1.0-cba878b70b2f8b52f1e031b41ac13b4e',

View File

@@ -13,6 +13,7 @@
import mock import mock
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from zun.common import exception from zun.common import exception
import zun.conf import zun.conf
@@ -23,10 +24,11 @@ from zun.volume import driver
CONF = zun.conf.CONF CONF = zun.conf.CONF
class VolumeDriverTestCase(base.TestCase): class CinderVolumeDriverTestCase(base.TestCase):
def setUp(self): 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_volume_id = 'fake-volume-id'
self.fake_devpath = '/fake-path' self.fake_devpath = '/fake-path'
self.fake_mountpoint = '/fake-mountpoint' self.fake_mountpoint = '/fake-mountpoint'
@@ -35,6 +37,7 @@ class VolumeDriverTestCase(base.TestCase):
'data': {'device_path': self.fake_devpath}, 'data': {'device_path': self.fake_devpath},
} }
self.volume = mock.MagicMock() self.volume = mock.MagicMock()
self.volume.uuid = self.fake_uuid
self.volume.volume_provider = 'cinder' self.volume.volume_provider = 'cinder'
self.volume.volume_id = self.fake_volume_id self.volume.volume_id = self.fake_volume_id
self.volume.container_path = self.fake_container_path self.volume.container_path = self.fake_container_path
@@ -55,7 +58,7 @@ class VolumeDriverTestCase(base.TestCase):
volume_driver.attach(self.context, self.volume) volume_driver.attach(self.context, self.volume)
mock_cinder_workflow.attach_volume.assert_called_once_with(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( mock_do_mount.assert_called_once_with(
self.fake_devpath, self.fake_mountpoint, CONF.volume.fstype) self.fake_devpath, self.fake_mountpoint, CONF.volume.fstype)
mock_cinder_workflow.detach_volume.assert_not_called() mock_cinder_workflow.detach_volume.assert_not_called()
@@ -112,7 +115,7 @@ class VolumeDriverTestCase(base.TestCase):
volume_driver.attach, self.context, self.volume) volume_driver.attach, self.context, self.volume)
mock_cinder_workflow.attach_volume.assert_called_once_with(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( mock_do_mount.assert_called_once_with(
self.fake_devpath, self.fake_mountpoint, CONF.volume.fstype) self.fake_devpath, self.fake_mountpoint, CONF.volume.fstype)
mock_cinder_workflow.detach_volume.assert_called_once_with(self.volume) 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) volume_driver.attach, self.context, self.volume)
mock_cinder_workflow.attach_volume.assert_called_once_with(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( mock_do_mount.assert_called_once_with(
self.fake_devpath, self.fake_mountpoint, CONF.volume.fstype) self.fake_devpath, self.fake_mountpoint, CONF.volume.fstype)
mock_cinder_workflow.detach_volume.assert_called_once_with(self.volume) 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) volume_driver.detach(self.context, self.volume)
mock_cinder_workflow.detach_volume.assert_called_once_with(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_do_unmount.assert_called_once_with(self.fake_mountpoint)
mock_rmtree.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_mountpoint, source)
self.assertEqual(self.fake_container_path, destination) 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('shutil.rmtree')
@mock.patch('zun.common.mount.get_mountpoint') @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_cinder_workflow.delete_volume.assert_called_once_with(self.volume)
mock_rmtree.assert_called_once_with(self.fake_mountpoint) 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)

View File

@@ -33,12 +33,11 @@ LOG = logging.getLogger(__name__)
CONF = zun.conf.CONF CONF = zun.conf.CONF
def driver(*args, **kwargs): def driver(driver_name, *args, **kwargs):
name = CONF.volume.driver LOG.info("Loading volume driver '%s'", driver_name)
LOG.info("Loading volume driver '%s'", name)
volume_driver = stevedore_driver.DriverManager( volume_driver = stevedore_driver.DriverManager(
"zun.volume.driver", "zun.volume.driver",
name, driver_name,
invoke_on_load=True, invoke_on_load=True,
invoke_args=args, invoke_args=args,
invoke_kwds=kwargs).driver invoke_kwds=kwargs).driver
@@ -84,6 +83,41 @@ class VolumeDriver(object):
raise NotImplementedError() 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): class Cinder(VolumeDriver):
supported_providers = [ supported_providers = [
@@ -105,7 +139,7 @@ class Cinder(VolumeDriver):
LOG.exception("Failed to detach volume") LOG.exception("Failed to detach volume")
def _mount_device(self, volume, devpath): def _mount_device(self, volume, devpath):
mountpoint = mount.get_mountpoint(volume.volume_id) mountpoint = mount.get_mountpoint(volume.uuid)
fileutils.ensure_tree(mountpoint) fileutils.ensure_tree(mountpoint)
mount.do_mount(devpath, mountpoint, CONF.volume.fstype) mount.do_mount(devpath, mountpoint, CONF.volume.fstype)
@@ -123,13 +157,13 @@ class Cinder(VolumeDriver):
def _unmount_device(self, volume): def _unmount_device(self, volume):
if hasattr(volume, 'connection_info'): if hasattr(volume, 'connection_info'):
mountpoint = mount.get_mountpoint(volume.volume_id) mountpoint = mount.get_mountpoint(volume.uuid)
mount.do_unmount(mountpoint) mount.do_unmount(mountpoint)
shutil.rmtree(mountpoint) shutil.rmtree(mountpoint)
@validate_volume_provider(supported_providers) @validate_volume_provider(supported_providers)
def bind_mount(self, context, volume): def bind_mount(self, context, volume):
mountpoint = mount.get_mountpoint(volume.volume_id) mountpoint = mount.get_mountpoint(volume.uuid)
return mountpoint, volume.container_path return mountpoint, volume.container_path
@validate_volume_provider(supported_providers) @validate_volume_provider(supported_providers)