From c7369adbe53091a0715f3b639c168c968e6367ef Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Wed, 30 Aug 2017 22:49:16 +0000 Subject: [PATCH] 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 --- setup.cfg | 3 + zun/tests/unit/volume/test_driver.py | 178 +++++++++++++++++++++++++++ zun/volume/driver.py | 110 +++++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 zun/tests/unit/volume/test_driver.py create mode 100644 zun/volume/driver.py diff --git a/setup.cfg b/setup.cfg index cb8b4edea..588c71806 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/zun/tests/unit/volume/test_driver.py b/zun/tests/unit/volume/test_driver.py new file mode 100644 index 000000000..cb616b59d --- /dev/null +++ b/zun/tests/unit/volume/test_driver.py @@ -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) diff --git a/zun/volume/driver.py b/zun/volume/driver.py new file mode 100644 index 000000000..97282f5c9 --- /dev/null +++ b/zun/volume/driver.py @@ -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