SMBFS: manageable volumes
This change allows the SMBFS driver to claim (manage) vhd/x images present on pre-configured shares. Note that images having backing (parent) files will be rejected. Managing snapshots will not be supported either. The images may reside in subdirectories (and get moved afterwards), which may be useful when having a folder dedicated to images that are to be imported in Cinder. All the logic is included in the new "RemoteFSManageableVolumesMixin" class so that any related driver may inherit it. For the record, this works out of the box for the NFS driver (special care has been taken in this sense). Implements: blueprint remotefs-manage-unmanage-volume Change-Id: I54695655e563d84e4fb1b76c42f0127c5fb909f7
This commit is contained in:
parent
5d0816b0d8
commit
ed945da6bf
@ -1001,6 +1001,10 @@ class RemoteFSNoSuitableShareFound(RemoteFSException):
|
||||
message = _("There is no share which can host %(volume_size)sG")
|
||||
|
||||
|
||||
class RemoteFSInvalidBackingFile(VolumeDriverException):
|
||||
message = _("File %(path)s has invalid backing file %(backing_file)s.")
|
||||
|
||||
|
||||
# NFS driver
|
||||
class NfsException(RemoteFSException):
|
||||
message = _("Unknown NFS exception")
|
||||
|
@ -15,6 +15,7 @@
|
||||
import collections
|
||||
import copy
|
||||
import os
|
||||
import re
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
@ -27,6 +28,7 @@ from cinder.tests.unit import fake_snapshot
|
||||
from cinder.tests.unit import fake_volume
|
||||
from cinder import utils
|
||||
from cinder.volume.drivers import remotefs
|
||||
from cinder.volume import utils as volume_utils
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@ -425,7 +427,7 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
|
||||
expected_basename_calls.append(mock.call(backing_file))
|
||||
mock_basename.assert_has_calls(expected_basename_calls)
|
||||
else:
|
||||
self.assertRaises(exception.RemoteFSException,
|
||||
self.assertRaises(exception.RemoteFSInvalidBackingFile,
|
||||
self._driver._qemu_img_info_base,
|
||||
mock.sentinel.image_path,
|
||||
fake_vol_name, basedir)
|
||||
@ -797,3 +799,332 @@ class RevertToSnapshotMixinTestCase(test.TestCase):
|
||||
self._fake_snapshot.volume)
|
||||
mock_read_info_file.assert_called_once_with(
|
||||
mock_local_path_vol_info.return_value)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class RemoteFSManageableVolumesTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(RemoteFSManageableVolumesTestCase, self).setUp()
|
||||
# We'll instantiate this directly for now.
|
||||
self._driver = remotefs.RemoteFSManageableVolumesMixin()
|
||||
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_mount_point_for_share', create=True)
|
||||
@mock.patch.object(os.path, 'isfile')
|
||||
def test_get_manageable_vol_location_invalid(self, mock_is_file,
|
||||
mock_get_mount_point):
|
||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
||||
self._driver._get_manageable_vol_location,
|
||||
{})
|
||||
|
||||
self._driver._mounted_shares = []
|
||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
||||
self._driver._get_manageable_vol_location,
|
||||
{'source-name': '//hots/share/img'})
|
||||
|
||||
self._driver._mounted_shares = ['//host/share']
|
||||
mock_get_mount_point.return_value = '/fake_mountpoint'
|
||||
mock_is_file.return_value = False
|
||||
|
||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
||||
self._driver._get_manageable_vol_location,
|
||||
{'source-name': '//host/share/subdir/img'})
|
||||
mock_is_file.assert_any_call(
|
||||
os.path.normpath('/fake_mountpoint/subdir/img'))
|
||||
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_mount_point_for_share', create=True)
|
||||
@mock.patch.object(os.path, 'isfile')
|
||||
def test_get_manageable_vol_location(self, mock_is_file,
|
||||
mock_get_mount_point):
|
||||
self._driver._mounted_shares = [
|
||||
'//host/share2/subdir',
|
||||
'//host/share/subdir',
|
||||
'host:/dir/subdir'
|
||||
]
|
||||
|
||||
mock_get_mount_point.return_value = '/fake_mountpoint'
|
||||
mock_is_file.return_value = True
|
||||
|
||||
location_info = self._driver._get_manageable_vol_location(
|
||||
{'source-name': 'host:/dir/subdir/import/img'})
|
||||
|
||||
exp_location_info = {
|
||||
'share': 'host:/dir/subdir',
|
||||
'mountpoint': mock_get_mount_point.return_value,
|
||||
'vol_local_path': '/fake_mountpoint/import/img',
|
||||
'vol_remote_path': 'host:/dir/subdir/import/img'
|
||||
}
|
||||
self.assertEqual(exp_location_info, location_info)
|
||||
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_mount_point_for_share', create=True)
|
||||
@mock.patch.object(os.path, 'isfile')
|
||||
@mock.patch.object(os.path, 'normpath', lambda x: x.replace('/', '\\'))
|
||||
@mock.patch.object(os.path, 'normcase', lambda x: x.lower())
|
||||
@mock.patch.object(os.path, 'join', lambda *args: '\\'.join(args))
|
||||
@mock.patch.object(os.path, 'sep', '\\')
|
||||
def test_get_manageable_vol_location_win32(self, mock_is_file,
|
||||
mock_get_mount_point):
|
||||
self._driver._mounted_shares = [
|
||||
'//host/share2/subdir',
|
||||
'//host/share/subdir',
|
||||
'host:/dir/subdir'
|
||||
]
|
||||
|
||||
mock_get_mount_point.return_value = r'c:\fake_mountpoint'
|
||||
mock_is_file.return_value = True
|
||||
|
||||
location_info = self._driver._get_manageable_vol_location(
|
||||
{'source-name': '//Host/share/Subdir/import/img'})
|
||||
|
||||
exp_location_info = {
|
||||
'share': '//host/share/subdir',
|
||||
'mountpoint': mock_get_mount_point.return_value,
|
||||
'vol_local_path': r'c:\fake_mountpoint\import\img',
|
||||
'vol_remote_path': r'\\host\share\subdir\import\img'
|
||||
}
|
||||
self.assertEqual(exp_location_info, location_info)
|
||||
|
||||
def test_get_managed_vol_exp_path(self):
|
||||
fake_vol = fake_volume.fake_volume_obj(mock.sentinel.context)
|
||||
vol_location = dict(mountpoint='fake-mountpoint')
|
||||
|
||||
exp_path = os.path.join(vol_location['mountpoint'],
|
||||
fake_vol.name)
|
||||
ret_val = self._driver._get_managed_vol_expected_path(
|
||||
fake_vol, vol_location)
|
||||
self.assertEqual(exp_path, ret_val)
|
||||
|
||||
@ddt.data(
|
||||
{'already_managed': True},
|
||||
{'qemu_side_eff': exception.RemoteFSInvalidBackingFile},
|
||||
{'qemu_side_eff': Exception},
|
||||
{'qemu_side_eff': [mock.Mock(backing_file=None,
|
||||
file_format='fakefmt')]},
|
||||
{'qemu_side_eff': [mock.Mock(backing_file='backing_file',
|
||||
file_format='raw')]}
|
||||
)
|
||||
@ddt.unpack
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_qemu_img_info', create=True)
|
||||
def test_check_unmanageable_volume(self, mock_qemu_info,
|
||||
qemu_side_eff=None,
|
||||
already_managed=False):
|
||||
mock_qemu_info.side_effect = qemu_side_eff
|
||||
|
||||
manageable = self._driver._is_volume_manageable(
|
||||
mock.sentinel.volume_path,
|
||||
already_managed=already_managed)[0]
|
||||
self.assertFalse(manageable)
|
||||
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_qemu_img_info', create=True)
|
||||
def test_check_manageable_volume(self, mock_qemu_info,
|
||||
qemu_side_eff=None,
|
||||
already_managed=False):
|
||||
mock_qemu_info.return_value = mock.Mock(
|
||||
backing_file=None,
|
||||
file_format='raw')
|
||||
|
||||
manageable = self._driver._is_volume_manageable(
|
||||
mock.sentinel.volume_path)[0]
|
||||
self.assertTrue(manageable)
|
||||
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_manageable_vol_location')
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_is_volume_manageable')
|
||||
def test_manage_existing_unmanageable(self, mock_check_manageable,
|
||||
mock_get_location):
|
||||
fake_vol = fake_volume.fake_volume_obj(mock.sentinel.context)
|
||||
|
||||
mock_get_location.return_value = dict(
|
||||
vol_local_path=mock.sentinel.local_path)
|
||||
mock_check_manageable.return_value = False, mock.sentinel.resason
|
||||
|
||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
||||
self._driver.manage_existing,
|
||||
fake_vol,
|
||||
mock.sentinel.existing_ref)
|
||||
mock_get_location.assert_called_once_with(mock.sentinel.existing_ref)
|
||||
mock_check_manageable.assert_called_once_with(
|
||||
mock.sentinel.local_path)
|
||||
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_manageable_vol_location')
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_is_volume_manageable')
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_set_rw_permissions', create=True)
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_managed_vol_expected_path')
|
||||
@mock.patch.object(os, 'rename')
|
||||
def test_manage_existing_manageable(self, mock_rename,
|
||||
mock_get_exp_path,
|
||||
mock_set_perm,
|
||||
mock_check_manageable,
|
||||
mock_get_location):
|
||||
fake_vol = fake_volume.fake_volume_obj(mock.sentinel.context)
|
||||
|
||||
mock_get_location.return_value = dict(
|
||||
vol_local_path=mock.sentinel.local_path,
|
||||
share=mock.sentinel.share)
|
||||
mock_check_manageable.return_value = True, None
|
||||
|
||||
exp_ret_val = {'provider_location': mock.sentinel.share}
|
||||
ret_val = self._driver.manage_existing(fake_vol,
|
||||
mock.sentinel.existing_ref)
|
||||
self.assertEqual(exp_ret_val, ret_val)
|
||||
|
||||
mock_get_exp_path.assert_called_once_with(
|
||||
fake_vol, mock_get_location.return_value)
|
||||
mock_set_perm.assert_called_once_with(mock.sentinel.local_path)
|
||||
mock_rename.assert_called_once_with(mock.sentinel.local_path,
|
||||
mock_get_exp_path.return_value)
|
||||
|
||||
@mock.patch.object(image_utils, 'qemu_img_info')
|
||||
def _get_rounded_manageable_image_size(self, mock_qemu_info):
|
||||
mock_qemu_info.return_value.virtual_size = 1 << 30 + 1
|
||||
exp_rounded_size_gb = 2
|
||||
|
||||
size = self._driver._get_rounded_manageable_image_size(
|
||||
mock.sentinel.image_path)
|
||||
self.assertEqual(exp_rounded_size_gb, size)
|
||||
|
||||
mock_qemu_info.assert_called_once_with(mock.sentinel.image_path)
|
||||
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_manageable_vol_location')
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_rounded_manageable_image_size')
|
||||
def test_manage_existing_get_size(self, mock_get_size,
|
||||
mock_get_location):
|
||||
mock_get_location.return_value = dict(
|
||||
vol_local_path=mock.sentinel.image_path)
|
||||
|
||||
size = self._driver.manage_existing_get_size(
|
||||
mock.sentinel.volume,
|
||||
mock.sentinel.existing_ref)
|
||||
self.assertEqual(mock_get_size.return_value, size)
|
||||
|
||||
mock_get_location.assert_called_once_with(mock.sentinel.existing_ref)
|
||||
mock_get_size.assert_called_once_with(mock.sentinel.image_path)
|
||||
|
||||
@ddt.data(
|
||||
{},
|
||||
{'managed_volume': mock.Mock(size=mock.sentinel.sz),
|
||||
'exp_size': mock.sentinel.sz,
|
||||
'manageable_check_ret_val': False,
|
||||
'exp_manageable': False},
|
||||
{'exp_size': None,
|
||||
'get_size_side_effect': Exception,
|
||||
'exp_manageable': False})
|
||||
@ddt.unpack
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_is_volume_manageable')
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_rounded_manageable_image_size')
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_mount_point_for_share', create=True)
|
||||
def test_get_manageable_volume(
|
||||
self, mock_get_mount_point,
|
||||
mock_get_size, mock_check_manageable,
|
||||
managed_volume=None,
|
||||
get_size_side_effect=(mock.sentinel.size_gb, ),
|
||||
manageable_check_ret_val=True,
|
||||
exp_size=mock.sentinel.size_gb,
|
||||
exp_manageable=True):
|
||||
share = '//host/share'
|
||||
mountpoint = '/fake-mountpoint'
|
||||
volume_path = '/fake-mountpoint/subdir/vol'
|
||||
|
||||
exp_ret_val = {
|
||||
'reference': {'source-name': '//host/share/subdir/vol'},
|
||||
'size': exp_size,
|
||||
'safe_to_manage': exp_manageable,
|
||||
'reason_not_safe': mock.ANY,
|
||||
'cinder_id': managed_volume.id if managed_volume else None,
|
||||
'extra_info': None,
|
||||
}
|
||||
|
||||
mock_get_size.side_effect = get_size_side_effect
|
||||
mock_check_manageable.return_value = (manageable_check_ret_val,
|
||||
mock.sentinel.reason)
|
||||
mock_get_mount_point.return_value = mountpoint
|
||||
|
||||
ret_val = self._driver._get_manageable_volume(
|
||||
share, volume_path, managed_volume)
|
||||
self.assertEqual(exp_ret_val, ret_val)
|
||||
|
||||
mock_check_manageable.assert_called_once_with(
|
||||
volume_path, already_managed=managed_volume is not None)
|
||||
mock_get_mount_point.assert_called_once_with(share)
|
||||
if managed_volume:
|
||||
mock_get_size.assert_not_called()
|
||||
else:
|
||||
mock_get_size.assert_called_once_with(volume_path)
|
||||
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_mount_point_for_share', create=True)
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_manageable_volume')
|
||||
@mock.patch.object(os, 'walk')
|
||||
@mock.patch.object(os.path, 'join', lambda *args: '/'.join(args))
|
||||
def test_get_share_manageable_volumes(
|
||||
self, mock_walk, mock_get_manageable_volume,
|
||||
mock_get_mount_point):
|
||||
mount_path = '/fake-mountpoint'
|
||||
mock_walk.return_value = [
|
||||
[mount_path, ['subdir'], ['volume-1.vhdx']],
|
||||
['/fake-mountpoint/subdir', [], ['volume-0', 'volume-3.vhdx']]]
|
||||
|
||||
mock_get_manageable_volume.side_effect = [
|
||||
Exception,
|
||||
mock.sentinel.managed_volume]
|
||||
|
||||
self._driver._MANAGEABLE_IMAGE_RE = re.compile('.*\.(?:vhdx)$')
|
||||
|
||||
managed_volumes = {'volume-1': mock.sentinel.vol1}
|
||||
|
||||
exp_manageable = [mock.sentinel.managed_volume]
|
||||
manageable_volumes = self._driver._get_share_manageable_volumes(
|
||||
mock.sentinel.share,
|
||||
managed_volumes)
|
||||
|
||||
self.assertEqual(exp_manageable, manageable_volumes)
|
||||
|
||||
mock_get_manageable_volume.assert_has_calls(
|
||||
[mock.call(mock.sentinel.share,
|
||||
'/fake-mountpoint/volume-1.vhdx',
|
||||
mock.sentinel.vol1),
|
||||
mock.call(mock.sentinel.share,
|
||||
'/fake-mountpoint/subdir/volume-3.vhdx',
|
||||
None)])
|
||||
|
||||
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
|
||||
'_get_share_manageable_volumes')
|
||||
@mock.patch.object(volume_utils, 'paginate_entries_list')
|
||||
def test_get_manageable_volumes(self, mock_paginate, mock_get_share_vols):
|
||||
fake_vol = fake_volume.fake_volume_obj(mock.sentinel.context)
|
||||
self._driver._mounted_shares = [mock.sentinel.share0,
|
||||
mock.sentinel.share1]
|
||||
|
||||
mock_get_share_vols.side_effect = [
|
||||
Exception, [mock.sentinel.manageable_vol]]
|
||||
|
||||
pagination_args = [
|
||||
mock.sentinel.marker, mock.sentinel.limit,
|
||||
mock.sentinel.offset, mock.sentinel.sort_keys,
|
||||
mock.sentinel.sort_dirs]
|
||||
ret_val = self._driver.get_manageable_volumes(
|
||||
[fake_vol], *pagination_args)
|
||||
|
||||
self.assertEqual(mock_paginate.return_value, ret_val)
|
||||
mock_paginate.assert_called_once_with(
|
||||
[mock.sentinel.manageable_vol], *pagination_args)
|
||||
|
||||
exp_managed_vols_dict = {fake_vol.name: fake_vol}
|
||||
mock_get_share_vols.assert_has_calls(
|
||||
[mock.call(share, exp_managed_vols_dict)
|
||||
for share in self._driver._mounted_shares])
|
||||
|
@ -285,7 +285,7 @@ class WindowsSmbFsTestCase(test.TestCase):
|
||||
|
||||
extensions = [
|
||||
".%s" % ext
|
||||
for ext in self._smbfs_driver._SUPPORTED_IMAGE_FORMATS]
|
||||
for ext in self._smbfs_driver._VALID_IMAGE_EXTENSIONS]
|
||||
possible_paths = [self._FAKE_VOLUME_PATH + ext
|
||||
for ext in extensions]
|
||||
mock_exists.assert_has_calls(
|
||||
@ -816,3 +816,15 @@ class WindowsSmbFsTestCase(test.TestCase):
|
||||
|
||||
mock_type = drv._get_vhd_type(qemu_subformat=False)
|
||||
self.assertEqual(mock_type, 3)
|
||||
|
||||
def test_get_managed_vol_expected_path(self):
|
||||
self._vhdutils.get_vhd_format.return_value = 'vhdx'
|
||||
vol_location = dict(vol_local_path=mock.sentinel.image_path,
|
||||
mountpoint=self._FAKE_MNT_POINT)
|
||||
|
||||
path = self._smbfs_driver._get_managed_vol_expected_path(
|
||||
self.volume, vol_location)
|
||||
self.assertEqual(self._FAKE_VOLUME_PATH, path)
|
||||
|
||||
self._vhdutils.get_vhd_format.assert_called_once_with(
|
||||
mock.sentinel.image_path)
|
||||
|
@ -18,6 +18,7 @@ import collections
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@ -764,10 +765,8 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
|
||||
}
|
||||
if not re.match(backing_file_template, info.backing_file,
|
||||
re.IGNORECASE):
|
||||
msg = _("File %(path)s has invalid backing file "
|
||||
"%(bfile)s, aborting.") % {'path': path,
|
||||
'bfile': info.backing_file}
|
||||
raise exception.RemoteFSException(msg)
|
||||
raise exception.RemoteFSInvalidBackingFile(
|
||||
path=path, backing_file=info.backing_file)
|
||||
|
||||
info.backing_file = os.path.basename(info.backing_file)
|
||||
|
||||
@ -1780,3 +1779,193 @@ class RevertToSnapshotMixin(object):
|
||||
# this class.
|
||||
self._delete(snapshot_path)
|
||||
self._do_create_snapshot(snapshot, backing_filename, snapshot_path)
|
||||
|
||||
|
||||
class RemoteFSManageableVolumesMixin(object):
|
||||
_SUPPORTED_IMAGE_FORMATS = ['raw', 'qcow2']
|
||||
_MANAGEABLE_IMAGE_RE = None
|
||||
|
||||
def _get_manageable_vol_location(self, existing_ref):
|
||||
if 'source-name' not in existing_ref:
|
||||
reason = _('The existing volume reference '
|
||||
'must contain "source-name".')
|
||||
raise exception.ManageExistingInvalidReference(
|
||||
existing_ref=existing_ref, reason=reason)
|
||||
|
||||
vol_remote_path = os.path.normcase(
|
||||
os.path.normpath(existing_ref['source-name']))
|
||||
|
||||
for mounted_share in self._mounted_shares:
|
||||
# We don't currently attempt to resolve hostnames. This could
|
||||
# be troublesome for some distributed shares, which may have
|
||||
# hostnames resolving to multiple addresses.
|
||||
norm_share = os.path.normcase(os.path.normpath(mounted_share))
|
||||
head, match, share_rel_path = vol_remote_path.partition(norm_share)
|
||||
if not (match and share_rel_path.startswith(os.path.sep)):
|
||||
continue
|
||||
|
||||
mountpoint = self._get_mount_point_for_share(mounted_share)
|
||||
vol_local_path = os.path.join(mountpoint,
|
||||
share_rel_path.lstrip(os.path.sep))
|
||||
|
||||
LOG.debug("Found mounted share referenced by %s.",
|
||||
vol_remote_path)
|
||||
|
||||
if os.path.isfile(vol_local_path):
|
||||
LOG.debug("Found volume %(path)s on share %(share)s.",
|
||||
dict(path=vol_local_path, share=mounted_share))
|
||||
return dict(share=mounted_share,
|
||||
mountpoint=mountpoint,
|
||||
vol_local_path=vol_local_path,
|
||||
vol_remote_path=vol_remote_path)
|
||||
else:
|
||||
LOG.error("Could not find volume %s on the "
|
||||
"specified share.", vol_remote_path)
|
||||
break
|
||||
|
||||
raise exception.ManageExistingInvalidReference(
|
||||
existing_ref=existing_ref, reason=_('Volume not found.'))
|
||||
|
||||
def _get_managed_vol_expected_path(self, volume, volume_location):
|
||||
# This may be overridden by the drivers.
|
||||
return os.path.join(volume_location['mountpoint'],
|
||||
volume.name)
|
||||
|
||||
def _is_volume_manageable(self, volume_path, already_managed=False):
|
||||
unmanageable_reason = None
|
||||
|
||||
if already_managed:
|
||||
return False, _('Volume already managed.')
|
||||
|
||||
try:
|
||||
img_info = self._qemu_img_info(volume_path, volume_name=None)
|
||||
except exception.RemoteFSInvalidBackingFile:
|
||||
return False, _("Backing file present.")
|
||||
except Exception:
|
||||
return False, _("Failed to open image.")
|
||||
|
||||
# We're double checking as some drivers do not validate backing
|
||||
# files through '_qemu_img_info'.
|
||||
if img_info.backing_file:
|
||||
return False, _("Backing file present.")
|
||||
|
||||
if img_info.file_format not in self._SUPPORTED_IMAGE_FORMATS:
|
||||
unmanageable_reason = _(
|
||||
"Unsupported image format: '%s'.") % img_info.file_format
|
||||
return False, unmanageable_reason
|
||||
|
||||
return True, None
|
||||
|
||||
def manage_existing(self, volume, existing_ref):
|
||||
LOG.info('Managing volume %(volume_id)s with ref %(ref)s',
|
||||
{'volume_id': volume.id, 'ref': existing_ref})
|
||||
|
||||
vol_location = self._get_manageable_vol_location(existing_ref)
|
||||
vol_local_path = vol_location['vol_local_path']
|
||||
|
||||
manageable, unmanageable_reason = self._is_volume_manageable(
|
||||
vol_local_path)
|
||||
|
||||
if not manageable:
|
||||
raise exception.ManageExistingInvalidReference(
|
||||
existing_ref=existing_ref, reason=unmanageable_reason)
|
||||
|
||||
expected_vol_path = self._get_managed_vol_expected_path(
|
||||
volume, vol_location)
|
||||
|
||||
self._set_rw_permissions(vol_local_path)
|
||||
|
||||
# This should be the last thing we do.
|
||||
if expected_vol_path != vol_local_path:
|
||||
LOG.info("Renaming imported volume image %(src)s to %(dest)s",
|
||||
dict(src=vol_location['vol_local_path'],
|
||||
dest=expected_vol_path))
|
||||
os.rename(vol_location['vol_local_path'],
|
||||
expected_vol_path)
|
||||
|
||||
return {'provider_location': vol_location['share']}
|
||||
|
||||
def _get_rounded_manageable_image_size(self, image_path):
|
||||
image_size = image_utils.qemu_img_info(
|
||||
image_path, run_as_root=self._execute_as_root).virtual_size
|
||||
return int(math.ceil(float(image_size) / units.Gi))
|
||||
|
||||
def manage_existing_get_size(self, volume, existing_ref):
|
||||
vol_location = self._get_manageable_vol_location(existing_ref)
|
||||
volume_path = vol_location['vol_local_path']
|
||||
return self._get_rounded_manageable_image_size(volume_path)
|
||||
|
||||
def unmanage(self, volume):
|
||||
pass
|
||||
|
||||
def _get_manageable_volume(self, share, volume_path, managed_volume=None):
|
||||
manageable, unmanageable_reason = self._is_volume_manageable(
|
||||
volume_path, already_managed=managed_volume is not None)
|
||||
size_gb = None
|
||||
if managed_volume:
|
||||
# We may not be able to query in-use images.
|
||||
size_gb = managed_volume.size
|
||||
else:
|
||||
try:
|
||||
size_gb = self._get_rounded_manageable_image_size(volume_path)
|
||||
except Exception:
|
||||
manageable = False
|
||||
unmanageable_reason = (unmanageable_reason or
|
||||
_("Failed to get size."))
|
||||
|
||||
mountpoint = self._get_mount_point_for_share(share)
|
||||
norm_mountpoint = os.path.normcase(os.path.normpath(mountpoint))
|
||||
norm_vol_path = os.path.normcase(os.path.normpath(volume_path))
|
||||
|
||||
ref = norm_vol_path.replace(norm_mountpoint, share).replace('\\', '/')
|
||||
manageable_volume = {
|
||||
'reference': {'source-name': ref},
|
||||
'size': size_gb,
|
||||
'safe_to_manage': manageable,
|
||||
'reason_not_safe': unmanageable_reason,
|
||||
'cinder_id': managed_volume.id if managed_volume else None,
|
||||
'extra_info': None,
|
||||
}
|
||||
return manageable_volume
|
||||
|
||||
def _get_share_manageable_volumes(self, share, managed_volumes):
|
||||
manageable_volumes = []
|
||||
mount_path = self._get_mount_point_for_share(share)
|
||||
|
||||
for dir_path, dir_names, file_names in os.walk(mount_path):
|
||||
for file_name in file_names:
|
||||
file_name = os.path.normcase(file_name)
|
||||
img_path = os.path.join(dir_path, file_name)
|
||||
# In the future, we may have the regex filtering images
|
||||
# as a config option.
|
||||
if (not self._MANAGEABLE_IMAGE_RE or
|
||||
self._MANAGEABLE_IMAGE_RE.match(file_name)):
|
||||
managed_volume = managed_volumes.get(
|
||||
os.path.splitext(file_name)[0])
|
||||
try:
|
||||
manageable_volume = self._get_manageable_volume(
|
||||
share, img_path, managed_volume)
|
||||
manageable_volumes.append(manageable_volume)
|
||||
except Exception as exc:
|
||||
LOG.error(
|
||||
"Failed to get manageable volume info: "
|
||||
"'%(image_path)s'. Exception: %(exc)s.",
|
||||
dict(image_path=img_path, exc=exc))
|
||||
return manageable_volumes
|
||||
|
||||
def get_manageable_volumes(self, cinder_volumes, marker, limit, offset,
|
||||
sort_keys, sort_dirs):
|
||||
manageable_volumes = []
|
||||
managed_volumes = {vol.name: vol for vol in cinder_volumes}
|
||||
|
||||
for share in self._mounted_shares:
|
||||
try:
|
||||
manageable_volumes += self._get_share_manageable_volumes(
|
||||
share, managed_volumes)
|
||||
except Exception as exc:
|
||||
LOG.error("Failed to get manageable volumes for "
|
||||
"share %(share)s. Exception: %(exc)s.",
|
||||
dict(share=share, exc=exc))
|
||||
|
||||
return volume_utils.paginate_entries_list(
|
||||
manageable_volumes, marker, limit, offset, sort_keys, sort_dirs)
|
||||
|
@ -14,6 +14,7 @@
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from os_brick.remotefs import windows_remotefs as remotefs_brick
|
||||
@ -95,6 +96,7 @@ CONF.set_default('reserved_percentage', 5)
|
||||
@interface.volumedriver
|
||||
class WindowsSmbfsDriver(remotefs_drv.RevertToSnapshotMixin,
|
||||
remotefs_drv.RemoteFSPoolMixin,
|
||||
remotefs_drv.RemoteFSManageableVolumesMixin,
|
||||
remotefs_drv.RemoteFSSnapDriverDistributed):
|
||||
VERSION = VERSION
|
||||
|
||||
@ -113,8 +115,13 @@ class WindowsSmbfsDriver(remotefs_drv.RevertToSnapshotMixin,
|
||||
|
||||
_MINIMUM_QEMU_IMG_VERSION = '1.6'
|
||||
|
||||
_SUPPORTED_IMAGE_FORMATS = [_DISK_FORMAT_VHD, _DISK_FORMAT_VHDX]
|
||||
_VALID_IMAGE_EXTENSIONS = _SUPPORTED_IMAGE_FORMATS
|
||||
_SUPPORTED_IMAGE_FORMATS = [_DISK_FORMAT_VHD,
|
||||
_DISK_FORMAT_VHD_LEGACY,
|
||||
_DISK_FORMAT_VHDX]
|
||||
_VALID_IMAGE_EXTENSIONS = [_DISK_FORMAT_VHD, _DISK_FORMAT_VHDX]
|
||||
_MANAGEABLE_IMAGE_RE = re.compile(
|
||||
'.*\.(?:%s)$' % '|'.join(_VALID_IMAGE_EXTENSIONS),
|
||||
re.IGNORECASE)
|
||||
|
||||
_always_use_temp_snap_when_cloning = False
|
||||
_thin_provisioning_support = True
|
||||
@ -277,7 +284,7 @@ class WindowsSmbfsDriver(remotefs_drv.RevertToSnapshotMixin,
|
||||
return local_path_template
|
||||
|
||||
def _lookup_local_volume_path(self, volume_path_template):
|
||||
for ext in self._SUPPORTED_IMAGE_FORMATS:
|
||||
for ext in self._VALID_IMAGE_EXTENSIONS:
|
||||
volume_path = (volume_path_template + '.' + ext
|
||||
if ext else volume_path_template)
|
||||
if os.path.exists(volume_path):
|
||||
@ -295,7 +302,7 @@ class WindowsSmbfsDriver(remotefs_drv.RevertToSnapshotMixin,
|
||||
|
||||
if volume_path:
|
||||
ext = os.path.splitext(volume_path)[1].strip('.').lower()
|
||||
if ext in self._SUPPORTED_IMAGE_FORMATS:
|
||||
if ext in self._VALID_IMAGE_EXTENSIONS:
|
||||
volume_format = ext
|
||||
else:
|
||||
# Hyper-V relies on file extensions so we're enforcing them.
|
||||
@ -611,3 +618,13 @@ class WindowsSmbfsDriver(remotefs_drv.RevertToSnapshotMixin,
|
||||
vhd_type = self._vhd_type_mapping[prov_type]
|
||||
|
||||
return vhd_type
|
||||
|
||||
def _get_managed_vol_expected_path(self, volume, volume_location):
|
||||
fmt = self._vhdutils.get_vhd_format(volume_location['vol_local_path'])
|
||||
return os.path.join(volume_location['mountpoint'],
|
||||
volume.name + ".%s" % fmt).lower()
|
||||
|
||||
def _set_rw_permissions(self, path):
|
||||
# The SMBFS driver does not manage file permissions. We chose
|
||||
# to let this up to the deployer.
|
||||
pass
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The SMBFS driver now supports the volume manage/unmanage feature. Images
|
||||
residing on preconfigured shares may be listed and managed by Cinder.
|
Loading…
Reference in New Issue
Block a user