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:
parent
a13a693313
commit
21ae28cc1a
@ -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.
|
||||
|
@ -75,6 +75,7 @@ zun.network.driver =
|
||||
|
||||
zun.volume.driver =
|
||||
cinder = zun.volume.driver:Cinder
|
||||
local = zun.volume.driver:Local
|
||||
|
||||
[extras]
|
||||
osprofiler =
|
||||
|
@ -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
|
||||
|
@ -201,6 +201,9 @@ mounts = {
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'type': {
|
||||
'type': ['string'],
|
||||
},
|
||||
'source': {
|
||||
'type': ['string'],
|
||||
},
|
||||
|
@ -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):
|
||||
|
@ -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'.
|
||||
|
@ -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.'),
|
||||
|
@ -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)
|
||||
|
@ -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)
|
@ -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'),
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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'}]}
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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'),
|
||||
}
|
||||
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user