# under the License.
import copy
+import functools
import os
import mock
+from oslo_utils import fileutils
from cinder import exception
from cinder.image import image_utils
from cinder import test
+from cinder.volume.drivers import remotefs
from cinder.volume.drivers import smbfs
+def requires_allocation_data_update(expected_size):
+ def wrapper(func):
+ @functools.wraps(func)
+ def inner(inst, *args, **kwargs):
+ with mock.patch.object(
+ inst._smbfs_driver,
+ 'update_disk_allocation_data') as fake_update:
+ func(inst, *args, **kwargs)
+ fake_update.assert_called_once_with(inst._FAKE_VOLUME,
+ expected_size)
+ return inner
+ return wrapper
+
+
class SmbFsTestCase(test.TestCase):
_FAKE_SHARE = '//1.2.3.4/share1'
+ _FAKE_SHARE_HASH = 'db0bf952c1734092b83e8990bd321131'
_FAKE_MNT_BASE = '/mnt'
_FAKE_VOLUME_NAME = 'volume-4f711859-4928-4cb7-801a-a50c37ceaccc'
_FAKE_TOTAL_SIZE = '2048'
'provider_location': _FAKE_SHARE,
'name': _FAKE_VOLUME_NAME,
'status': 'available'}
- _FAKE_MNT_POINT = os.path.join(_FAKE_MNT_BASE, 'fake_hash')
+ _FAKE_MNT_POINT = os.path.join(_FAKE_MNT_BASE, _FAKE_SHARE_HASH)
_FAKE_VOLUME_PATH = os.path.join(_FAKE_MNT_POINT, _FAKE_VOLUME_NAME)
_FAKE_SNAPSHOT_ID = '5g811859-4928-4cb7-801a-a50c37ceacba'
_FAKE_SNAPSHOT = {'id': _FAKE_SNAPSHOT_ID,
_FAKE_SHARE_OPTS = '-o username=Administrator,password=12345'
_FAKE_OPTIONS_DICT = {'username': 'Administrator',
'password': '12345'}
+ _FAKE_ALLOCATION_DATA_PATH = os.path.join('fake_dir',
+ 'fake_allocation_data')
- _FAKE_LISTDIR = [_FAKE_VOLUME_NAME, _FAKE_VOLUME_NAME + '.vhd',
- _FAKE_VOLUME_NAME + '.vhdx', 'fake_folder']
_FAKE_SMBFS_CONFIG = mock.MagicMock()
_FAKE_SMBFS_CONFIG.smbfs_oversub_ratio = 2
_FAKE_SMBFS_CONFIG.smbfs_used_ratio = 0.5
return_value=self._FAKE_MNT_POINT)
self._smbfs_driver._execute = mock.Mock()
self._smbfs_driver.base = self._FAKE_MNT_BASE
+ self._smbfs_driver._alloc_info_file_path = (
+ self._FAKE_ALLOCATION_DATA_PATH)
+
+ def _get_fake_allocation_data(self):
+ return {self._FAKE_SHARE_HASH: {
+ 'total_allocated': self._FAKE_TOTAL_ALLOCATED}}
+
+ @mock.patch.object(smbfs, 'open', create=True)
+ @mock.patch('os.path.exists')
+ @mock.patch.object(fileutils, 'ensure_tree')
+ @mock.patch('json.load')
+ def _test_setup_allocation_data(self, mock_json_load, mock_ensure_tree,
+ mock_exists, mock_open,
+ allocation_data_exists=False):
+ mock_exists.return_value = allocation_data_exists
+ self._smbfs_driver._update_allocation_data_file = mock.Mock()
+
+ self._smbfs_driver._setup_allocation_data()
+
+ if allocation_data_exists:
+ fd = mock_open.return_value.__enter__.return_value
+ mock_json_load.assert_called_once_with(fd)
+ self.assertEqual(mock_json_load.return_value,
+ self._smbfs_driver._allocation_data)
+ else:
+ mock_ensure_tree.assert_called_once_with(
+ os.path.dirname(self._FAKE_ALLOCATION_DATA_PATH))
+ update_func = self._smbfs_driver._update_allocation_data_file
+ update_func.assert_called_once_with()
+
+ def test_setup_allocation_data_file_unexisting(self):
+ self._test_setup_allocation_data()
+
+ def test_setup_allocation_data_file_existing(self):
+ self._test_setup_allocation_data(allocation_data_exists=True)
+
+ def _test_update_allocation_data(self, virtual_size_gb=None,
+ volume_exists=True):
+ self._smbfs_driver._update_allocation_data_file = mock.Mock()
+ update_func = self._smbfs_driver._update_allocation_data_file
+
+ fake_alloc_data = self._get_fake_allocation_data()
+ if volume_exists:
+ fake_alloc_data[self._FAKE_SHARE_HASH][
+ self._FAKE_VOLUME_NAME] = self._FAKE_VOLUME['size']
+
+ self._smbfs_driver._allocation_data = fake_alloc_data
+ self._smbfs_driver.update_disk_allocation_data(self._FAKE_VOLUME,
+ virtual_size_gb)
+
+ vol_allocated_size = fake_alloc_data[self._FAKE_SHARE_HASH].get(
+ self._FAKE_VOLUME_NAME, None)
+ if not virtual_size_gb:
+ expected_total_allocated = (self._FAKE_TOTAL_ALLOCATED -
+ self._FAKE_VOLUME['size'])
+
+ self.assertIsNone(vol_allocated_size)
+ else:
+ expected_total_allocated = (self._FAKE_TOTAL_ALLOCATED +
+ virtual_size_gb -
+ self._FAKE_VOLUME['size'])
+ self.assertEqual(virtual_size_gb, vol_allocated_size)
+
+ update_func.assert_called_once_with()
+
+ self.assertEqual(
+ expected_total_allocated,
+ fake_alloc_data[self._FAKE_SHARE_HASH]['total_allocated'])
+
+ def test_update_allocation_data_volume_deleted(self):
+ self._test_update_allocation_data()
+
+ def test_update_allocation_data_volume_extended(self):
+ self._test_update_allocation_data(
+ virtual_size_gb=self._FAKE_VOLUME['size'] + 1)
+
+ def test_update_allocation_data_volume_created(self):
+ self._test_update_allocation_data(
+ virtual_size_gb=self._FAKE_VOLUME['size'])
+
+ @requires_allocation_data_update(expected_size=None)
def test_delete_volume(self):
drv = self._smbfs_driver
fake_vol_info = self._FAKE_VOLUME_PATH + '.info'
self._smbfs_driver._mounted_shares = mounted_shares
self._smbfs_driver._is_share_eligible = mock.Mock(
return_value=eligible_shares)
- fake_capacity_info = ((2, 1, 5), (2, 1, 4), (2, 1, 1))
- self._smbfs_driver._get_capacity_info = mock.Mock(
- side_effect=fake_capacity_info)
+ self._smbfs_driver._get_total_allocated = mock.Mock(
+ side_effect=[3, 2, 1])
if not mounted_shares:
self.assertRaises(exception.SmbfsNoSharesMounted,
fake_convert:
if extend_failed:
self.assertRaises(exception.ExtendVolumeError,
- drv._extend_volume,
+ drv.extend_volume,
self._FAKE_VOLUME, mock.sentinel.new_size)
else:
- drv._extend_volume(
- self._FAKE_VOLUME,
- mock.sentinel.new_size)
+ drv.extend_volume(self._FAKE_VOLUME, mock.sentinel.new_size)
+
if image_format in (drv._DISK_FORMAT_VHDX,
drv._DISK_FORMAT_VHD_LEGACY):
fake_tmp_path = self._FAKE_VOLUME_PATH + '.tmp'
fake_resize.assert_called_once_with(
self._FAKE_VOLUME_PATH, mock.sentinel.new_size)
+ @requires_allocation_data_update(expected_size=mock.sentinel.new_size)
def test_extend_volume(self):
self._test_extend_volume()
def test_extend_volume_failed(self):
self._test_extend_volume(extend_failed=True)
+ @requires_allocation_data_update(expected_size=mock.sentinel.new_size)
def test_extend_vhd_volume(self):
self._test_extend_volume(image_format='vpc')
def test_check_extend_volume_uneligible_share(self):
self._test_check_extend_support(is_eligible=False)
+ @requires_allocation_data_update(expected_size=_FAKE_VOLUME['size'])
+ @mock.patch.object(remotefs.RemoteFSSnapDriver, 'create_volume')
+ def test_create_volume_base(self, mock_create_volume):
+ self._smbfs_driver.create_volume(self._FAKE_VOLUME)
+ mock_create_volume.assert_called_once_with(self._FAKE_VOLUME)
+
+ @requires_allocation_data_update(expected_size=_FAKE_VOLUME['size'])
+ @mock.patch.object(smbfs.SmbfsDriver,
+ '_create_volume_from_snapshot')
+ def test_create_volume_from_snapshot(self, mock_create_volume):
+ self._smbfs_driver.create_volume_from_snapshot(self._FAKE_VOLUME,
+ self._FAKE_SNAPSHOT)
+ mock_create_volume.assert_called_once_with(self._FAKE_VOLUME,
+ self._FAKE_SNAPSHOT)
+
+ @requires_allocation_data_update(expected_size=_FAKE_VOLUME['size'])
+ @mock.patch.object(smbfs.SmbfsDriver, '_create_cloned_volume')
+ def test_create_cloned_volume(self, mock_create_volume):
+ self._smbfs_driver.create_cloned_volume(self._FAKE_VOLUME,
+ mock.sentinel.src_vol)
+ mock_create_volume.assert_called_once_with(self._FAKE_VOLUME,
+ mock.sentinel.src_vol)
+
def test_create_volume_from_in_use_snapshot(self):
fake_snapshot = {'status': 'in-use'}
self.assertRaises(
fake_block_size = 4096.0
fake_total_blocks = 1024
fake_avail_blocks = 512
- fake_total_allocated = fake_total_blocks * fake_block_size
fake_df = ('%s %s %s' % (fake_block_size, fake_total_blocks,
fake_avail_blocks), None)
- fake_du = (str(fake_total_allocated), None)
self._smbfs_driver._get_mount_point_for_share = mock.Mock(
return_value=self._FAKE_MNT_POINT)
- self._smbfs_driver._execute = mock.Mock(
- side_effect=(fake_df, fake_du))
+ self._smbfs_driver._get_total_allocated = mock.Mock(
+ return_value=self._FAKE_TOTAL_ALLOCATED)
+ self._smbfs_driver._execute.return_value = fake_df
ret_val = self._smbfs_driver._get_capacity_info(self._FAKE_SHARE)
expected = (fake_block_size * fake_total_blocks,
fake_block_size * fake_avail_blocks,
- fake_total_allocated)
+ self._FAKE_TOTAL_ALLOCATED)
self.assertEqual(expected, ret_val)
_FAKE_MNT_POINT = os.path.join(_FAKE_MNT_BASE, 'fake_hash')
_FAKE_VOLUME_NAME = 'volume-4f711859-4928-4cb7-801a-a50c37ceaccc'
_FAKE_SNAPSHOT_NAME = _FAKE_VOLUME_NAME + '-snapshot.vhdx'
- _FAKE_VOLUME_PATH = os.path.join(_FAKE_MNT_POINT,
- _FAKE_VOLUME_NAME)
_FAKE_SNAPSHOT_PATH = os.path.join(_FAKE_MNT_POINT,
_FAKE_SNAPSHOT_NAME)
_FAKE_TOTAL_SIZE = '2048'
_FAKE_SHARE_OPTS = '-o username=Administrator,password=12345'
_FAKE_VOLUME_PATH = os.path.join(_FAKE_MNT_POINT,
_FAKE_VOLUME_NAME + '.vhdx')
- _FAKE_LISTDIR = [_FAKE_VOLUME_NAME + '.vhd',
- _FAKE_VOLUME_NAME + '.vhdx', 'fake_folder']
def setUp(self):
super(WindowsSmbFsTestCase, self).setUp()
self._FAKE_TOTAL_ALLOCATED]]
self.assertEqual(expected_ret_val, ret_val)
- def test_get_total_allocated(self):
- fake_listdir = mock.Mock(side_effect=[self._FAKE_LISTDIR,
- self._FAKE_LISTDIR[:-1]])
- fake_folder_path = os.path.join(self._FAKE_SHARE, 'fake_folder')
- fake_isdir = lambda x: x == fake_folder_path
- self._smbfs_driver._remotefsclient.is_symlink = mock.Mock(
- return_value=False)
- fake_getsize = mock.Mock(return_value=self._FAKE_VOLUME['size'])
- self._smbfs_driver.vhdutils.get_vhd_size = mock.Mock(
- return_value={'VirtualSize': 1})
-
- with mock.patch.multiple('os.path', isdir=fake_isdir,
- getsize=fake_getsize):
- with mock.patch('os.listdir', fake_listdir):
- ret_val = self._smbfs_driver._get_total_allocated(
- self._FAKE_SHARE)
- self.assertEqual(4, ret_val)
-
def _test_get_img_info(self, backing_file=None):
self._smbfs_driver.vhdutils.get_vhd_parent_path.return_value = (
backing_file)
# License for the specific language governing permissions and limitations
# under the License.
+import decorator
+
+import inspect
+import json
import os
from os_brick.remotefs import remotefs
from oslo_concurrency import processutils as putils
from oslo_config import cfg
from oslo_log import log as logging
+from oslo_utils import fileutils
from oslo_utils import units
from cinder import exception
cfg.StrOpt('smbfs_shares_config',
default='/etc/cinder/smbfs_shares',
help='File with the list of available smbfs shares.'),
+ cfg.StrOpt('smbfs_allocation_info_file_path',
+ default='$state_path/allocation_data',
+ help=('The path of the automatically generated file containing '
+ 'information about volume disk space allocation.')),
cfg.StrOpt('smbfs_default_volume_format',
default='qcow2',
choices=['raw', 'qcow2', 'vhd', 'vhdx'],
CONF.register_opts(volume_opts)
+def update_allocation_data(delete=False):
+ @decorator.decorator
+ def wrapper(func, inst, *args, **kwargs):
+ ret_val = func(inst, *args, **kwargs)
+
+ call_args = inspect.getcallargs(func, inst, *args, **kwargs)
+ volume = call_args['volume']
+ requested_size = call_args.get('size_gb', None)
+
+ if delete:
+ allocated_size_gb = None
+ else:
+ allocated_size_gb = requested_size or volume['size']
+
+ inst.update_disk_allocation_data(volume, allocated_size_gb)
+ return ret_val
+ return wrapper
+
+
class SmbfsDriver(remotefs_drv.RemoteFSSnapDriver):
"""SMBFS based cinder volume driver."""
smbfs_mount_point_base=self.base,
smbfs_mount_options=opts)
self.img_suffix = None
+ self._alloc_info_file_path = CONF.smbfs_allocation_info_file_path
def _qemu_img_info(self, path, volume_name):
return super(SmbfsDriver, self)._qemu_img_info_base(
self.shares = {} # address : options
self._ensure_shares_mounted()
+ self._setup_allocation_data()
+
+ def _setup_allocation_data(self):
+ if not os.path.exists(self._alloc_info_file_path):
+ fileutils.ensure_tree(
+ os.path.dirname(self._alloc_info_file_path))
+ self._allocation_data = {}
+ self._update_allocation_data_file()
+ else:
+ with open(self._alloc_info_file_path, 'r') as f:
+ self._allocation_data = json.load(f)
+
+ def update_disk_allocation_data(self, volume, virtual_size_gb=None):
+ volume_name = volume['name']
+ smbfs_share = volume['provider_location']
+ if smbfs_share:
+ share_hash = self._get_hash_str(smbfs_share)
+ else:
+ return
+
+ share_alloc_data = self._allocation_data.get(share_hash, {})
+ old_virtual_size = share_alloc_data.get(volume_name, 0)
+ total_allocated = share_alloc_data.get('total_allocated', 0)
+
+ if virtual_size_gb:
+ share_alloc_data[volume_name] = virtual_size_gb
+ total_allocated += virtual_size_gb - old_virtual_size
+ elif share_alloc_data.get(volume_name):
+ # The volume is deleted.
+ del share_alloc_data[volume_name]
+ total_allocated -= old_virtual_size
+
+ share_alloc_data['total_allocated'] = total_allocated
+ self._allocation_data[share_hash] = share_alloc_data
+ self._update_allocation_data_file()
+
+ def _update_allocation_data_file(self):
+ with open(self._alloc_info_file_path, 'w') as f:
+ json.dump(self._allocation_data, f)
+
+ def _get_total_allocated(self, smbfs_share):
+ share_hash = self._get_hash_str(smbfs_share)
+ share_alloc_data = self._allocation_data.get(share_hash, {})
+ total_allocated = share_alloc_data.get('total_allocated', 0) << 30
+ return float(total_allocated)
def local_path(self, volume):
"""Get volume path (mounted locally fs path) for given volume.
return volume_format
@remotefs_drv.locked_volume_id_operation
+ @update_allocation_data(delete=True)
def delete_volume(self, volume):
"""Deletes a logical volume."""
if not volume['provider_location']:
volume_path, str(volume_size * units.Gi),
run_as_root=True)
+ @remotefs_drv.locked_volume_id_operation
+ @update_allocation_data()
+ def create_volume(self, volume):
+ return super(SmbfsDriver, self).create_volume(volume)
+
def _do_create_volume(self, volume):
"""Create a volume on given smbfs_share.
total_available = block_size * blocks_avail
total_size = block_size * blocks_total
- du, _ = self._execute('du', '-sb', '--apparent-size', '--exclude',
- '*snapshot*', mount_point, run_as_root=True)
- total_allocated = float(du.split()[0])
+ total_allocated = self._get_total_allocated(smbfs_share)
return total_size, total_available, total_allocated
def _find_share(self, volume_size_in_gib):
for smbfs_share in self._mounted_shares:
if not self._is_share_eligible(smbfs_share, volume_size_in_gib):
continue
- total_allocated = self._get_capacity_info(smbfs_share)[2]
+ total_allocated = self._get_total_allocated(smbfs_share)
if target_share is not None:
if target_share_reserved > total_allocated:
target_share = smbfs_share
raise exception.InvalidVolume(err_msg)
@remotefs_drv.locked_volume_id_operation
+ @update_allocation_data()
def extend_volume(self, volume, size_gb):
LOG.info(_LI('Extending volume %s.'), volume['id'])
self._extend_volume(volume, size_gb)
'extend volume %s to %sG.'
% (volume['id'], size_gb))
+ @remotefs_drv.locked_volume_id_operation
+ @update_allocation_data()
+ def create_volume_from_snapshot(self, volume, snapshot):
+ return self._create_volume_from_snapshot(volume, snapshot)
+
def _copy_volume_from_snapshot(self, snapshot, volume, volume_size):
"""Copy data from snapshot to destination volume.
reason=(_("Expected volume size was %d") % volume['size'])
+ (_(" but size is now %d.") % virt_size))
+ @remotefs_drv.locked_volume_id_operation
+ @update_allocation_data()
+ def create_cloned_volume(self, volume, src_vref):
+ """Creates a clone of the specified volume."""
+ return self._create_cloned_volume(volume, src_vref)
+
def _ensure_share_mounted(self, smbfs_share):
mnt_flags = []
if self.shares.get(smbfs_share) is not None:
import os
-import re
import sys
from oslo_config import cfg
from cinder import exception
from cinder.i18n import _, _LI
from cinder.image import image_utils
-from cinder import utils
+from cinder.volume.drivers import remotefs as remotefs_drv
from cinder.volume.drivers import smbfs
from cinder.volume.drivers.windows import remotefs
from cinder.volume.drivers.windows import vhdutils
CONF = cfg.CONF
CONF.set_default('smbfs_shares_config', r'C:\OpenStack\smbfs_shares.txt')
+CONF.set_default('smbfs_allocation_info_file_path',
+ r'C:\OpenStack\allocation_data.txt')
CONF.set_default('smbfs_mount_point_base', r'C:\OpenStack\_mnt')
CONF.set_default('smbfs_default_volume_format', 'vhd')
'allocated': total_allocated})
return [float(x) for x in return_value]
- def _get_total_allocated(self, smbfs_share):
- elements = os.listdir(smbfs_share)
- total_allocated = 0
- for element in elements:
- element_path = os.path.join(smbfs_share, element)
- if not self._remotefsclient.is_symlink(element_path):
- if "snapshot" in element:
- continue
- if re.search(r'\.vhdx?$', element):
- total_allocated += self.vhdutils.get_vhd_size(
- element_path)['VirtualSize']
- continue
- if os.path.isdir(element_path):
- total_allocated += self._get_total_allocated(element_path)
- continue
- total_allocated += os.path.getsize(element_path)
-
- return total_allocated
-
def _img_commit(self, snapshot_path):
self.vhdutils.merge_vhd(snapshot_path)
self._delete(snapshot_path)
def _do_extend_volume(self, volume_path, size_gb, volume_name=None):
self.vhdutils.resize_vhd(volume_path, size_gb * units.Gi)
- @utils.synchronized('smbfs', external=False)
+ @remotefs_drv.locked_volume_id_operation
def copy_volume_to_image(self, context, volume, image_service, image_meta):
"""Copy the volume to the specified image."""