Introduce Cinder volume driver
* Define the volume driver interface that all volume drivers need to inherit. Currently, the interface has three methods: attach, detach, and bind_mount. Attach should connect to the volume and mount the volume to a specific path in the filesystem for containers to bindmount. Detach should do the opposite of attach. Bind_mount returns the host and container paths that the binding should use. * Implement the first volume driver for Cinder. This driver is for mounting/unmounting Cinder volumes. It uses the CinderWorkflow API to attach/detach Cinder volumes and using the mounting utility to mount the attached volumes to the filsystem. Partial-Implements: blueprint direct-cinder-integration Change-Id: I602bdf7127f298a2193d3143b05188ec8ebfb64e
This commit is contained in:
parent
18b3b059c4
commit
c7369adbe5
@ -82,6 +82,9 @@ zun.image.driver =
|
|||||||
zun.network.driver =
|
zun.network.driver =
|
||||||
kuryr = zun.network.kuryr_network:KuryrNetwork
|
kuryr = zun.network.kuryr_network:KuryrNetwork
|
||||||
|
|
||||||
|
zun.volume.driver =
|
||||||
|
cinder = zun.volume.driver:Cinder
|
||||||
|
|
||||||
tempest.test_plugins =
|
tempest.test_plugins =
|
||||||
zun_tests = zun.tests.tempest.plugin:ZunTempestPlugin
|
zun_tests = zun.tests.tempest.plugin:ZunTempestPlugin
|
||||||
|
|
||||||
|
178
zun/tests/unit/volume/test_driver.py
Normal file
178
zun/tests/unit/volume/test_driver.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
|
||||||
|
from zun.common import exception
|
||||||
|
import zun.conf
|
||||||
|
from zun.tests import base
|
||||||
|
from zun.volume import driver
|
||||||
|
|
||||||
|
|
||||||
|
CONF = zun.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class VolumeDriverTestCase(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(VolumeDriverTestCase, self).setUp()
|
||||||
|
self.fake_volume_id = 'fake-volume-id'
|
||||||
|
self.fake_devpath = '/fake-path'
|
||||||
|
self.fake_mountpoint = '/fake-mountpoint'
|
||||||
|
self.fake_container_path = '/fake-container-path'
|
||||||
|
self.fake_conn_info = {
|
||||||
|
'data': {'device_path': self.fake_devpath},
|
||||||
|
}
|
||||||
|
self.volume = mock.MagicMock()
|
||||||
|
self.volume.volume_id = self.fake_volume_id
|
||||||
|
self.volume.container_path = self.fake_container_path
|
||||||
|
self.volume.connection_info = jsonutils.dumps(self.fake_conn_info)
|
||||||
|
|
||||||
|
@mock.patch('zun.common.mount.do_mount')
|
||||||
|
@mock.patch('oslo_utils.fileutils.ensure_tree')
|
||||||
|
@mock.patch('zun.common.mount.get_mountpoint')
|
||||||
|
@mock.patch('zun.volume.cinder_workflow.CinderWorkflow')
|
||||||
|
def test_attach(self, mock_cinder_workflow_cls, mock_get_mountpoint,
|
||||||
|
mock_ensure_tree, mock_do_mount):
|
||||||
|
mock_cinder_workflow = mock.MagicMock()
|
||||||
|
mock_cinder_workflow_cls.return_value = mock_cinder_workflow
|
||||||
|
mock_cinder_workflow.attach_volume.return_value = self.fake_devpath
|
||||||
|
mock_get_mountpoint.return_value = self.fake_mountpoint
|
||||||
|
|
||||||
|
volume_driver = driver.Cinder(self.context, 'cinder')
|
||||||
|
volume_driver.attach(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_do_mount.assert_called_once_with(
|
||||||
|
self.fake_devpath, self.fake_mountpoint, CONF.volume.fstype)
|
||||||
|
mock_cinder_workflow.detach_volume.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch('zun.common.mount.do_mount')
|
||||||
|
@mock.patch('oslo_utils.fileutils.ensure_tree')
|
||||||
|
@mock.patch('zun.common.mount.get_mountpoint')
|
||||||
|
@mock.patch('zun.volume.cinder_workflow.CinderWorkflow')
|
||||||
|
def test_attach_unknown_provider(self, mock_cinder_workflow_cls,
|
||||||
|
mock_get_mountpoint, mock_ensure_tree,
|
||||||
|
mock_do_mount):
|
||||||
|
self.assertRaises(exception.ZunException,
|
||||||
|
driver.Cinder, self.context, 'unknown')
|
||||||
|
|
||||||
|
@mock.patch('zun.common.mount.do_mount')
|
||||||
|
@mock.patch('oslo_utils.fileutils.ensure_tree')
|
||||||
|
@mock.patch('zun.common.mount.get_mountpoint')
|
||||||
|
@mock.patch('zun.volume.cinder_workflow.CinderWorkflow')
|
||||||
|
def test_attach_fail_attach(self, mock_cinder_workflow_cls,
|
||||||
|
mock_get_mountpoint, mock_ensure_tree,
|
||||||
|
mock_do_mount):
|
||||||
|
mock_cinder_workflow = mock.MagicMock()
|
||||||
|
mock_cinder_workflow_cls.return_value = mock_cinder_workflow
|
||||||
|
mock_cinder_workflow.attach_volume.side_effect = \
|
||||||
|
exception.ZunException()
|
||||||
|
mock_get_mountpoint.return_value = self.fake_mountpoint
|
||||||
|
|
||||||
|
volume_driver = driver.Cinder(self.context, 'cinder')
|
||||||
|
self.assertRaises(exception.ZunException,
|
||||||
|
volume_driver.attach, self.volume)
|
||||||
|
|
||||||
|
mock_cinder_workflow.attach_volume.assert_called_once_with(self.volume)
|
||||||
|
mock_get_mountpoint.assert_not_called()
|
||||||
|
mock_do_mount.assert_not_called()
|
||||||
|
mock_cinder_workflow.detach_volume.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch('zun.common.mount.do_mount')
|
||||||
|
@mock.patch('oslo_utils.fileutils.ensure_tree')
|
||||||
|
@mock.patch('zun.common.mount.get_mountpoint')
|
||||||
|
@mock.patch('zun.volume.cinder_workflow.CinderWorkflow')
|
||||||
|
def test_attach_fail_mount(self, mock_cinder_workflow_cls,
|
||||||
|
mock_get_mountpoint, mock_ensure_tree,
|
||||||
|
mock_do_mount):
|
||||||
|
mock_cinder_workflow = mock.MagicMock()
|
||||||
|
mock_cinder_workflow_cls.return_value = mock_cinder_workflow
|
||||||
|
mock_cinder_workflow.attach_volume.return_value = self.fake_devpath
|
||||||
|
mock_get_mountpoint.return_value = self.fake_mountpoint
|
||||||
|
mock_do_mount.side_effect = exception.ZunException()
|
||||||
|
|
||||||
|
volume_driver = driver.Cinder(self.context, 'cinder')
|
||||||
|
self.assertRaises(exception.ZunException,
|
||||||
|
volume_driver.attach, 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_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)
|
||||||
|
|
||||||
|
@mock.patch('zun.common.mount.do_mount')
|
||||||
|
@mock.patch('oslo_utils.fileutils.ensure_tree')
|
||||||
|
@mock.patch('zun.common.mount.get_mountpoint')
|
||||||
|
@mock.patch('zun.volume.cinder_workflow.CinderWorkflow')
|
||||||
|
def test_attach_fail_mount_and_detach(self, mock_cinder_workflow_cls,
|
||||||
|
mock_get_mountpoint,
|
||||||
|
mock_ensure_tree,
|
||||||
|
mock_do_mount):
|
||||||
|
class TestException1(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TestException2(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
mock_cinder_workflow = mock.MagicMock()
|
||||||
|
mock_cinder_workflow_cls.return_value = mock_cinder_workflow
|
||||||
|
mock_cinder_workflow.attach_volume.return_value = self.fake_devpath
|
||||||
|
mock_get_mountpoint.return_value = self.fake_mountpoint
|
||||||
|
mock_do_mount.side_effect = TestException1()
|
||||||
|
mock_cinder_workflow.detach_volume.side_effect = TestException2()
|
||||||
|
|
||||||
|
volume_driver = driver.Cinder(self.context, 'cinder')
|
||||||
|
self.assertRaises(TestException1,
|
||||||
|
volume_driver.attach, 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_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)
|
||||||
|
|
||||||
|
@mock.patch('zun.common.mount.do_unmount')
|
||||||
|
@mock.patch('zun.common.mount.get_mountpoint')
|
||||||
|
@mock.patch('zun.volume.cinder_workflow.CinderWorkflow')
|
||||||
|
def test_detach(self, mock_cinder_workflow_cls, mock_get_mountpoint,
|
||||||
|
mock_do_unmount):
|
||||||
|
mock_cinder_workflow = mock.MagicMock()
|
||||||
|
mock_cinder_workflow_cls.return_value = mock_cinder_workflow
|
||||||
|
mock_cinder_workflow.detach_volume.return_value = self.fake_devpath
|
||||||
|
mock_get_mountpoint.return_value = self.fake_mountpoint
|
||||||
|
|
||||||
|
volume_driver = driver.Cinder(self.context, 'cinder')
|
||||||
|
volume_driver.detach(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_do_unmount.assert_called_once_with(
|
||||||
|
self.fake_devpath, self.fake_mountpoint)
|
||||||
|
|
||||||
|
@mock.patch('zun.common.mount.get_mountpoint')
|
||||||
|
@mock.patch('zun.volume.cinder_workflow.CinderWorkflow')
|
||||||
|
def test_bind_mount(self, mock_cinder_workflow_cls, mock_get_mountpoint):
|
||||||
|
mock_cinder_workflow = mock.MagicMock()
|
||||||
|
mock_cinder_workflow_cls.return_value = mock_cinder_workflow
|
||||||
|
mock_get_mountpoint.return_value = self.fake_mountpoint
|
||||||
|
|
||||||
|
volume_driver = driver.Cinder(self.context, 'cinder')
|
||||||
|
source, destination = volume_driver.bind_mount(self.volume)
|
||||||
|
|
||||||
|
self.assertEqual(self.fake_mountpoint, source)
|
||||||
|
self.assertEqual(self.fake_container_path, destination)
|
||||||
|
mock_get_mountpoint.assert_called_once_with(self.fake_volume_id)
|
110
zun/volume/driver.py
Normal file
110
zun/volume/driver.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import six
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
from oslo_utils import excutils
|
||||||
|
from oslo_utils import fileutils
|
||||||
|
from stevedore import driver as stevedore_driver
|
||||||
|
|
||||||
|
from zun.common import exception
|
||||||
|
from zun.common.i18n import _
|
||||||
|
from zun.common import mount
|
||||||
|
import zun.conf
|
||||||
|
from zun.volume import cinder_workflow
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF = zun.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def driver(*args, **kwargs):
|
||||||
|
name = CONF.volume.driver
|
||||||
|
LOG.info("Loading volume driver '%s'", name)
|
||||||
|
volume_driver = stevedore_driver.DriverManager(
|
||||||
|
"zun.volume.driver",
|
||||||
|
name,
|
||||||
|
invoke_on_load=True,
|
||||||
|
invoke_args=args,
|
||||||
|
invoke_kwds=kwargs).driver
|
||||||
|
if not isinstance(volume_driver, VolumeDriver):
|
||||||
|
raise exception.ZunException(_("Invalid volume driver type"))
|
||||||
|
return volume_driver
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class VolumeDriver(object):
|
||||||
|
"""The base class that all Volume classes should inherit from."""
|
||||||
|
|
||||||
|
# Subclass should overwrite this list.
|
||||||
|
supported_providers = []
|
||||||
|
|
||||||
|
def __init__(self, context, provider):
|
||||||
|
if provider not in self.supported_providers:
|
||||||
|
msg = _("Unsupported volume provider '%s'") % provider
|
||||||
|
raise exception.ZunException(msg)
|
||||||
|
|
||||||
|
self.context = context
|
||||||
|
self.provider = provider
|
||||||
|
|
||||||
|
def attach(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def detach(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def bind_mount(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class Cinder(VolumeDriver):
|
||||||
|
|
||||||
|
supported_providers = [
|
||||||
|
'cinder'
|
||||||
|
]
|
||||||
|
|
||||||
|
def attach(self, volume):
|
||||||
|
cinder = cinder_workflow.CinderWorkflow(self.context)
|
||||||
|
devpath = cinder.attach_volume(volume)
|
||||||
|
try:
|
||||||
|
self._mount_device(volume, devpath)
|
||||||
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception("Failed to mount device")
|
||||||
|
try:
|
||||||
|
cinder.detach_volume(volume)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Failed to detach volume")
|
||||||
|
|
||||||
|
def _mount_device(self, volume, devpath):
|
||||||
|
mountpoint = mount.get_mountpoint(volume.volume_id)
|
||||||
|
fileutils.ensure_tree(mountpoint)
|
||||||
|
mount.do_mount(devpath, mountpoint, CONF.volume.fstype)
|
||||||
|
|
||||||
|
def detach(self, volume):
|
||||||
|
self._unmount_device(volume)
|
||||||
|
cinder = cinder_workflow.CinderWorkflow(self.context)
|
||||||
|
cinder.detach_volume(volume)
|
||||||
|
|
||||||
|
def _unmount_device(self, volume):
|
||||||
|
conn_info = jsonutils.loads(volume.connection_info)
|
||||||
|
devpath = conn_info['data']['device_path']
|
||||||
|
mountpoint = mount.get_mountpoint(volume.volume_id)
|
||||||
|
mount.do_unmount(devpath, mountpoint)
|
||||||
|
|
||||||
|
def bind_mount(self, volume):
|
||||||
|
mountpoint = mount.get_mountpoint(volume.volume_id)
|
||||||
|
return mountpoint, volume.container_path
|
Loading…
x
Reference in New Issue
Block a user