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 =
|
||||
kuryr = zun.network.kuryr_network:KuryrNetwork
|
||||
|
||||
zun.volume.driver =
|
||||
cinder = zun.volume.driver:Cinder
|
||||
|
||||
tempest.test_plugins =
|
||||
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