Merge "Support file injection on container"

This commit is contained in:
Zuul 2018-08-15 21:42:11 +00:00 committed by Gerrit Code Review
commit 1f7526faa6
18 changed files with 284 additions and 46 deletions

View File

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

View File

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

View File

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

View File

@ -201,6 +201,9 @@ mounts = {
'items': {
'type': 'object',
'properties': {
'type': {
'type': ['string'],
},
'source': {
'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.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):

View File

@ -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'.

View File

@ -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.'),

View File

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

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)
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'),

View File

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

View File

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

View File

@ -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'}]}

View File

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

View File

@ -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'),
}

View File

@ -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',

View File

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

View File

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