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:
Hongbin Lu 2017-08-30 22:49:16 +00:00 committed by Hongbin Lu
parent 18b3b059c4
commit c7369adbe5
3 changed files with 291 additions and 0 deletions

View File

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

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