Remove Windows drivers

These drivers depend on os-win, which was already retired due to
retirement of WinStackers project.

os-win depends on pkg_resources which was removed from setuptools
recently.

Also remove flake8-logging-format, because it currently requires
pkg_resources[1].

[1] https://github.com/globality-corp/flake8-logging-format/pull/78

Change-Id: I6bdf6e284e427cd6415cfe40dc1a75e9fe4a1885
Signed-off-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
This commit is contained in:
Takashi Kajinami
2026-02-15 13:38:57 +09:00
parent d8525886bb
commit 143c5c12fb
37 changed files with 41 additions and 3844 deletions
+1 -33
View File
@@ -25,7 +25,6 @@ import abc
import hashlib
import json
import os
import sys
import eventlet
from oslo_config import cfg
@@ -41,11 +40,6 @@ from cinder import objects
from cinder.objects import fields
from cinder.volume import volume_utils
if sys.platform == 'win32':
from os_win import utilsfactory as os_win_utilsfactory
else:
os_win_utilsfactory = None
LOG = logging.getLogger(__name__)
backup_opts = [
@@ -151,13 +145,6 @@ class ChunkedBackupDriver(driver.BackupDriver, metaclass=abc.ABCMeta):
self._get_compressor(CONF.backup_compression_algorithm)
self.support_force_delete = True
if sys.platform == 'win32' and self.chunk_size_bytes % 4096:
# The chunk size must be a multiple of the sector size. In order
# to fail out early and avoid attaching the disks, we'll just
# enforce the chunk size to be a multiple of 4096.
err = _("Invalid chunk size. It must be a multiple of 4096.")
raise exception.InvalidConfigurationValue(message=err)
def _get_object_writer(self, container, object_name, extra_metadata=None):
"""Return writer proxy-wrapped to execute methods in native thread."""
writer = self.get_object_writer(container, object_name, extra_metadata)
@@ -497,12 +484,6 @@ class ChunkedBackupDriver(driver.BackupDriver, metaclass=abc.ABCMeta):
extra_usage_info=
object_meta)
def _get_win32_phys_disk_size(self, disk_path):
win32_diskutils = os_win_utilsfactory.get_diskutils()
disk_number = win32_diskutils.get_device_number_from_device_name(
disk_path)
return win32_diskutils.get_disk_size(disk_number)
def _calculate_sha(self, data):
"""Calculate SHA256 of a data chunk.
@@ -557,14 +538,6 @@ class ChunkedBackupDriver(driver.BackupDriver, metaclass=abc.ABCMeta):
'backup. Do a full backup.')
raise exception.InvalidBackup(reason=err)
win32_disk_size = None
if sys.platform == 'win32':
# When dealing with Windows physical disks, we need the exact
# size of the disk. Attempting to read passed this boundary will
# lead to an IOError exception. At the same time, we cannot
# seek to the end of file.
win32_disk_size = self._get_win32_phys_disk_size(volume_file.name)
(object_meta, object_sha256, extra_metadata, container,
volume_size_bytes) = self._prepare_backup(backup)
@@ -604,12 +577,7 @@ class ChunkedBackupDriver(driver.BackupDriver, metaclass=abc.ABCMeta):
LOG.debug('Cancel the backup process of %s.', backup.id)
break
data_offset = volume_file.tell()
if win32_disk_size is not None:
read_bytes = min(self.chunk_size_bytes,
win32_disk_size - data_offset)
else:
read_bytes = self.chunk_size_bytes
read_bytes = self.chunk_size_bytes
data = volume_file.read(read_bytes)
if data == b'':
+3 -4
View File
@@ -105,7 +105,7 @@ MAPPING = {
'cinder.backup.drivers.google.GoogleBackupDriver':
'cinder.backup.drivers.gcs.GoogleBackupDriver',
}
SERVICE_PGRP = '' if os.name == 'nt' else os.getpgrp()
SERVICE_PGRP = os.getpgrp()
# TODO(geguileo): Once Eventlet issue #432 gets fixed we can just tpool.execute
@@ -788,17 +788,16 @@ class BackupManager(manager.SchedulerDependentManager):
# with native threads proxy-wrapping the device file object.
try:
device_path = attach_info['device']['path']
open_mode = 'rb+' if os.name == 'nt' else 'wb'
if (isinstance(device_path, str) and
not os.path.isdir(device_path)):
if secure_enabled:
with open(device_path, open_mode) as device_file:
with open(device_path, 'wb') as device_file:
backup_service.restore(backup, volume.id,
tpool.Proxy(device_file),
volume_is_new)
else:
with utils.temporary_chown(device_path):
with open(device_path, open_mode) as device_file:
with open(device_path, 'wb') as device_file:
backup_service.restore(backup, volume.id,
tpool.Proxy(device_file),
volume_is_new)
+1 -25
View File
@@ -46,6 +46,7 @@ REMOVED_DRVRS = [
"hpe_lefthand",
"sheepdog",
"zfssa",
"windows"
]
@@ -186,30 +187,6 @@ class Checks(uc.UpgradeCommands):
'and is removed in Wallaby release.')
return uc.Result(SUCCESS)
def _check_legacy_windows_config(self) -> uc.Result:
"""Checks to ensure that the Windows driver path is properly updated.
The WindowsDriver was renamed in the Queens release to
WindowsISCSIDriver to avoid confusion with the SMB driver.
The backwards compatibility for this has now been removed, so
any cinder.conf settings still using
cinder.volume.drivers.windows.windows.WindowsDriver
must now be updated to use
cinder.volume.drivers.windows.iscsi.WindowsISCSIDriver.
"""
for volume_driver in _get_enabled_drivers():
if (volume_driver ==
"cinder.volume.drivers.windows.windows.WindowsDriver"):
return uc.Result(
FAILURE,
'Setting volume_driver to '
'cinder.volume.drivers.windows.windows.WindowsDriver '
'is no longer supported. Please update to use '
'cinder.volume.drivers.windows.iscsi.WindowsISCSIDriver '
'in cinder.conf.')
return uc.Result(SUCCESS)
def _check_removed_drivers(self) -> uc.Result:
"""Checks to ensure that no removed drivers are configured.
@@ -267,7 +244,6 @@ class Checks(uc.UpgradeCommands):
# added in Stein
('Backup Driver Path', _check_backup_module),
('Use of Policy File', _check_policy_file),
('Windows Driver Path', _check_legacy_windows_config),
('Removed Drivers', _check_removed_drivers),
# added in Train
('Periodic Interval Use', _check_periodic_interval),
+3 -57
View File
@@ -17,22 +17,12 @@
"""Starter script for Cinder Volume."""
import logging as python_logging
import os
import re
import shlex
import sys
import eventlet
import eventlet.tpool
# Monkey patching must go before the oslo.log import, otherwise
# oslo.context will not use greenthread thread local and all greenthreads
# will share the same context.
if os.name == 'nt':
# eventlet monkey patching the os module causes subprocess.Popen to fail
# on Windows when using pipes due to missing non-blocking IO support.
eventlet.monkey_patch(os=False)
else:
eventlet.monkey_patch()
eventlet.monkey_patch()
# Monkey patch the original current_thread to use the up-to-date _active
# global variable. See https://bugs.launchpad.net/bugs/1863021 and
# https://github.com/eventlet/eventlet/issues/592
@@ -53,10 +43,8 @@ from oslo_reports import opts as gmr_opts
from cinder.common import config # noqa
from cinder.common import constants
from cinder.db import api as session
from cinder import exception
from cinder import i18n
i18n.enable_lazy()
from cinder.i18n import _
from cinder import objects
from cinder import service
from cinder import utils
@@ -132,42 +120,7 @@ def _notify_service_started() -> None:
service_started = True
def _launch_services_win32() -> None:
if CONF.backend_name and CONF.backend_name not in CONF.enabled_backends:
msg = _('The explicitly passed backend name "%(backend_name)s" is not '
'among the enabled backends: %(enabled_backends)s.')
raise exception.InvalidInput(
reason=msg % dict(backend_name=CONF.backend_name,
enabled_backends=CONF.enabled_backends))
# We'll avoid spawning a subprocess if a single backend is requested.
single_backend_name = (CONF.enabled_backends[0]
if len(CONF.enabled_backends) == 1
else CONF.backend_name)
if single_backend_name:
launcher = service.get_launcher()
_launch_service(launcher, single_backend_name)
elif CONF.enabled_backends:
# We're using the 'backend_name' argument, requesting a certain backend
# and constructing the service object within the child process.
launcher = service.WindowsProcessLauncher()
py_script_re = re.compile(r'.*\.py\w?$')
backend: str
for backend in filter(None, CONF.enabled_backends):
cmd = sys.argv + ['--backend_name=%s' % backend]
# Recent setuptools versions will trim '-script.py' and '.exe'
# extensions from sys.argv[0].
if py_script_re.match(sys.argv[0]):
cmd = [sys.executable] + cmd
launcher.add_process(cmd)
_notify_service_started()
_ensure_service_started()
launcher.wait()
def _launch_services_posix() -> None:
def _launch_services() -> None:
launcher = service.get_launcher()
backend: str
@@ -199,11 +152,4 @@ def main() -> None:
'drivers is not supported since Ocata.')
sys.exit(1)
if os.name == 'nt':
# We cannot use oslo.service to spawn multiple services on Windows.
# It relies on forking, which is not available on Windows.
# Furthermore, service objects are unmarshallable objects that are
# passed to subprocesses.
_launch_services_win32()
else:
_launch_services_posix()
_launch_services()
-5
View File
@@ -19,8 +19,6 @@ import errno
import glob
import inspect
import os
import re
import sys
from typing import Callable, Optional
import uuid
@@ -68,9 +66,6 @@ class Coordinator(object):
def _get_file_path(self, backend_url):
if backend_url.startswith('file://'):
path = backend_url[7:]
# Copied from TooZ's _normalize_path to get the same path they use
if sys.platform == 'win32':
path = re.sub(r'\\(?=\w:\\)', '', os.path.normpath(path))
return os.path.abspath(os.path.join(path, self.prefix))
return None
+1 -5
View File
@@ -192,8 +192,6 @@ def qemu_img_info(
cmd.append('--force-share')
cmd.append(path)
if os.name == 'nt':
cmd = cmd[2:]
out, _err = utils.execute(*cmd, run_as_root=run_as_root,
prlimit=QEMU_IMG_LIMITS)
info = imageutils.QemuImgInfo(out, format='json')
@@ -211,8 +209,6 @@ def qemu_img_info(
if force_share:
cmd.append('--force-share')
cmd.append(path)
if os.name == 'nt':
cmd = cmd[2:]
try:
out, _err = utils.execute(*cmd, run_as_root=run_as_root,
prlimit=QEMU_IMG_LIMITS)
@@ -1134,7 +1130,7 @@ def fetch_to_volume_format(context: context.RequestContext,
@contextlib.contextmanager
def chown_if_needed(volume_path: str) -> Generator[None, None, None]:
if os.name == 'nt' or os.access(volume_path, os.R_OK):
if os.access(volume_path, os.R_OK):
yield
else:
with utils.temporary_chown(volume_path):
-6
View File
@@ -194,10 +194,6 @@ from cinder.volume.drivers.veritas_access import veritas_iscsi as \
from cinder.volume.drivers.vmware import vmdk as \
cinder_volume_drivers_vmware_vmdk
from cinder.volume.drivers import vzstorage as cinder_volume_drivers_vzstorage
from cinder.volume.drivers.windows import iscsi as \
cinder_volume_drivers_windows_iscsi
from cinder.volume.drivers.windows import smbfs as \
cinder_volume_drivers_windows_smbfs
from cinder.volume.drivers.yadro import tatlin_common as \
cinder_volume_drivers_yadro_tatlincommon
from cinder.volume.drivers.zadara import zadara as \
@@ -452,8 +448,6 @@ def list_opts():
cinder_volume_drivers_vastdata_driver.VASTDATA_OPTS,
cinder_volume_drivers_vmware_vmdk.vmdk_opts,
cinder_volume_drivers_vzstorage.vzstorage_opts,
cinder_volume_drivers_windows_iscsi.windows_opts,
cinder_volume_drivers_windows_smbfs.volume_opts,
cinder_volume_drivers_yadro_tatlincommon.tatlin_opts,
cinder_volume_drivers_zadara_zadara.common.zadara_opts,
cinder_volume_manager.volume_backend_opts,
+1 -69
View File
@@ -21,9 +21,6 @@
import inspect
import os
import random
import subprocess
import sys
import time
from typing import Optional
from oslo_concurrency import processutils
@@ -52,11 +49,6 @@ from cinder import rpc
from cinder import version
from cinder.volume import volume_utils
if os.name == 'nt':
from os_win import utilsfactory as os_win_utilsfactory
else:
os_win_utilsfactory = None
LOG = logging.getLogger(__name__)
@@ -644,65 +636,5 @@ def wait() -> None:
rpc.cleanup()
class Launcher(object):
def __init__(self):
self.launch_service = serve
self.wait = wait
def get_launcher() -> service.ProcessLauncher:
# Note(lpetrut): ProcessLauncher uses green pipes which fail on Windows
# due to missing support of non-blocking I/O pipes. For this reason, the
# service must be spawned differently on Windows, using the ServiceLauncher
# class instead.
if os.name == 'nt':
return Launcher()
else:
return process_launcher()
class WindowsProcessLauncher(object):
def __init__(self):
self._processutils = os_win_utilsfactory.get_processutils()
self._workers = []
self._worker_job_handles = []
self._signal_handler = service.SignalHandler()
self._add_signal_handlers()
LOG.warning("Support for Windows operating systems is deprecated.")
def add_process(self, cmd):
LOG.info("Starting subprocess: %s", cmd)
worker = subprocess.Popen(cmd)
try:
job_handle = self._processutils.kill_process_on_job_close(
worker.pid)
except Exception:
LOG.exception("Could not associate child process "
"with a job, killing it.")
worker.kill()
raise
self._worker_job_handles.append(job_handle)
self._workers.append(worker)
def _add_signal_handlers(self):
self._signal_handler.add_handler('SIGINT', self._terminate)
self._signal_handler.add_handler('SIGTERM', self._terminate)
def _terminate(self, *args):
# We've already assigned win32 job objects to child processes,
# requesting them to stop once all the job handles are closed.
# When this process dies, so will the child processes.
LOG.info("Received request to terminate.")
sys.exit(1)
def wait(self):
pids = [worker.pid for worker in self._workers]
if pids:
self._processutils.wait_for_multiple_processes(pids,
wait_all=True)
# By sleeping here, we allow signal handlers to be executed.
time.sleep(0)
return process_launcher()
+1 -10
View File
@@ -22,19 +22,10 @@
:platform: Unix
"""
import os
import sys
import eventlet
# Monkey patching must go before the oslo.log import, otherwise
# oslo.context will not use greenthread thread local and all greenthreads
# will share the same context.
if os.name == 'nt':
# eventlet monkey patching the os module causes subprocess.Popen to fail
# on Windows when using pipes due to missing non-blocking IO support.
eventlet.monkey_patch(os=False)
else:
eventlet.monkey_patch()
eventlet.monkey_patch()
# Monkey patch the original current_thread to use the up-to-date _active
# global variable. See https://bugs.launchpad.net/bugs/1863021 and
# https://github.com/eventlet/eventlet/issues/592
@@ -30,7 +30,6 @@ from oslo_config import cfg
from swiftclient import client as swift
import zstd
from cinder.backup import chunkeddriver
from cinder.backup.drivers import swift as swift_dr
from cinder import context
from cinder import db
@@ -1057,71 +1056,3 @@ class BackupSwiftTestCase(test.TestCase):
admin_context = context.get_admin_context()
swift_dr.SwiftBackupDriver(admin_context)
mock_initialize.assert_not_called()
class WindowsBackupSwiftTestCase(BackupSwiftTestCase):
# We're running all the parent class tests, while doing
# some patching in order to simulate Windows behavior.
def setUp(self):
self._mock_utilsfactory = mock.Mock()
platform_patcher = mock.patch('sys.platform', 'win32')
platform_patcher.start()
self.addCleanup(platform_patcher.stop)
super(WindowsBackupSwiftTestCase, self).setUp()
read = self.volume_file.read
def win32_read(sz):
# We're simulating the Windows behavior.
if self.volume_file.tell() > fake_get_size():
raise IOError()
return read(sz)
read_patcher = mock.patch.object(
self.volume_file, 'read', win32_read)
read_patcher.start()
self.addCleanup(read_patcher.stop)
def fake_get_size(*args, **kwargs):
pos = self.volume_file.tell()
sz = self.volume_file.seek(0, 2)
self.volume_file.seek(pos)
return sz
self._disk_size_getter_mocker = mock.patch.object(
swift_dr.SwiftBackupDriver,
'_get_win32_phys_disk_size',
fake_get_size)
self._disk_size_getter_mocker.start()
self.addCleanup(self._disk_size_getter_mocker.stop)
def test_invalid_chunk_size(self):
self.flags(backup_swift_object_size=1000)
# We expect multiples of 4096
self.assertRaises(exception.InvalidConfigurationValue,
swift_dr.SwiftBackupDriver,
self.ctxt)
@mock.patch.object(chunkeddriver, 'os_win_utilsfactory', create=True)
def test_get_phys_disk_size(self, mock_utilsfactory):
# We're patching this method in setUp, so we need to
# retrieve the original one. Note that we'll get an unbound
# method.
service = swift_dr.SwiftBackupDriver(self.ctxt)
get_disk_size = self._disk_size_getter_mocker.temp_original
disk_utils = mock_utilsfactory.get_diskutils.return_value
disk_utils.get_device_number_from_device_name.return_value = (
mock.sentinel.dev_num)
disk_utils.get_disk_size.return_value = mock.sentinel.disk_size
disk_size = get_disk_size(service, mock.sentinel.disk_path)
self.assertEqual(mock.sentinel.disk_size, disk_size)
disk_utils.get_device_number_from_device_name.assert_called_once_with(
mock.sentinel.disk_path)
disk_utils.get_disk_size.assert_called_once_with(
mock.sentinel.dev_num)
+7 -19
View File
@@ -1372,12 +1372,9 @@ class BackupTestCase(BaseBackupTest):
@mock.patch('cinder.utils.temporary_chown')
@mock.patch('builtins.open', wraps=open)
@mock.patch.object(os.path, 'isdir', return_value=False)
@ddt.data({'os_name': 'nt', 'exp_open_mode': 'rb+'},
{'os_name': 'posix', 'exp_open_mode': 'wb'})
@ddt.unpack
def test_restore_backup(self, mock_isdir, mock_open,
mock_temporary_chown, mock_get_conn,
os_name, exp_open_mode):
mock_temporary_chown, mock_get_conn):
"""Test normal backup restoration."""
vol_size = 1
vol_id = self._create_volume_db_entry(status='restoring-backup',
@@ -1398,10 +1395,9 @@ class BackupTestCase(BaseBackupTest):
'_attach_device')
mock_attach_device.return_value = attach_info
with mock.patch('os.name', os_name):
self.backup_mgr.restore_backup(self.ctxt, backup, vol_id, False)
self.backup_mgr.restore_backup(self.ctxt, backup, vol_id, False)
mock_open.assert_called_once_with('/dev/null', exp_open_mode)
mock_open.assert_called_once_with('/dev/null', 'wb')
mock_temporary_chown.assert_called_once_with('/dev/null')
mock_get_conn.assert_called_once_with(False, enforce_multipath=False)
vol.status = 'available'
@@ -1422,16 +1418,12 @@ class BackupTestCase(BaseBackupTest):
@mock.patch('cinder.utils.temporary_chown')
@mock.patch('builtins.open', wraps=open)
@mock.patch.object(os.path, 'isdir', return_value=False)
@ddt.data({'os_name': 'nt', 'exp_open_mode': 'rb+'},
{'os_name': 'posix', 'exp_open_mode': 'wb'})
@ddt.unpack
def test_restore_backup_new_volume(self,
mock_isdir,
mock_open,
mock_temporary_chown,
mock_get_conn,
os_name,
exp_open_mode):
mock_get_conn):
"""Test normal backup restoration."""
vol_size = 1
vol_id = self._create_volume_db_entry(
@@ -1458,16 +1450,13 @@ class BackupTestCase(BaseBackupTest):
self.mock_object(self.backup_mgr, '_detach_device')
mock_attach_device.return_value = attach_info
with mock.patch('os.name', os_name):
self.backup_mgr.restore_backup(self.ctxt, backup, new_vol_id,
False)
self.backup_mgr.restore_backup(self.ctxt, backup, new_vol_id, False)
backup.status = "restoring"
db.backup_update(self.ctxt, backup.id, {"status": "restoring"})
vol.status = 'available'
vol.obj_reset_changes()
with mock.patch('os.name', os_name):
self.backup_mgr.restore_backup(self.ctxt, backup, vol2_id, False)
self.backup_mgr.restore_backup(self.ctxt, backup, vol2_id, False)
vol2.refresh()
old_src_backup_id = vol2.metadata["src_backup_id"]
@@ -1476,8 +1465,7 @@ class BackupTestCase(BaseBackupTest):
db.volume_update(self.ctxt, vol2.id, {"status": "restoring-backup"})
vol2.obj_reset_changes()
with mock.patch('os.name', os_name):
self.backup_mgr.restore_backup(self.ctxt, backup2, vol2_id, False)
self.backup_mgr.restore_backup(self.ctxt, backup2, vol2_id, False)
vol2.status = 'available'
vol2.obj_reset_changes()
+3 -21
View File
@@ -160,26 +160,6 @@ class TestCinderStatus(testtools.TestCase):
expected = uc.Code.FAILURE
self.assertEqual(expected, result.code)
def test_check_legacy_win_conf(self):
self._set_volume_driver(
'cinder.volume.drivers.windows.iscsi.WindowsISCSIDriver',
'winiscsi')
result = self.checks._check_legacy_windows_config()
self.assertEqual(uc.Code.SUCCESS, result.code)
def test_check_legacy_win_conf_fail(self):
self._set_volume_driver(
'cinder.volume.drivers.windows.windows.WindowsDriver',
'winiscsi')
result = self.checks._check_legacy_windows_config()
self.assertEqual(uc.Code.FAILURE, result.code)
self.assertIn('Please update to use', result.details)
def test_check_legacy_win_conf_no_drivers(self):
self._set_config('enabled_backends', None)
result = self.checks._check_legacy_windows_config()
self.assertEqual(uc.Code.SUCCESS, result.code)
def test_check_removed_drivers(self):
self._set_volume_driver(
'cinder.volume.drivers.lvm.LVMVolumeDriver',
@@ -196,7 +176,9 @@ class TestCinderStatus(testtools.TestCase):
'HPELeftHandISCSIDriver',
'cinder.volume.drivers.sheepdog.SheepdogDriver',
'cinder.volume.drivers.zfssa.zfssaiscsi.ZFSSAISCSIDriver',
'cinder.volume.drivers.zfssa.zfssanfs.ZFSSANFSDriver')
'cinder.volume.drivers.zfssa.zfssanfs.ZFSSANFSDriver',
'cinder.volume.drivers.windows.iscsi.WindowsISCSIDriver',
'cinder.volume.drivers.windows.smbfs.WindowsSmbfsDriver')
def test_check_removed_drivers_fail(self, volume_driver):
self._set_volume_driver(
volume_driver,
+2 -134
View File
@@ -185,14 +185,12 @@ class TestCinderSchedulerCmd(test.TestCase):
service_wait.assert_called_once_with()
class TestCinderVolumeCmdPosix(test.TestCase):
class TestCinderVolumeCmd(test.TestCase):
def setUp(self):
super(TestCinderVolumeCmdPosix, self).setUp()
super(TestCinderVolumeCmd, self).setUp()
sys.argv = ['cinder-volume']
self.patch('os.name', 'posix')
@mock.patch('cinder.service.get_launcher')
@mock.patch('cinder.service.Service.create')
@mock.patch('cinder.utils.monkey_patch')
@@ -233,136 +231,6 @@ class TestCinderVolumeCmdPosix(test.TestCase):
launcher.wait.assert_called_once_with()
@ddt.ddt
@test.testtools.skipIf(sys.platform == 'darwin', 'Not supported on macOS')
class TestCinderVolumeCmdWin32(test.TestCase):
def setUp(self):
super(TestCinderVolumeCmdWin32, self).setUp()
sys.argv = ['cinder-volume']
self._mock_win32_proc_launcher = mock.Mock()
self.patch('os.name', 'nt')
self.patch('cinder.service.WindowsProcessLauncher',
lambda *args, **kwargs: self._mock_win32_proc_launcher)
@mock.patch('cinder.service.get_launcher')
@mock.patch('cinder.service.Service.create')
@mock.patch('cinder.utils.monkey_patch')
@mock.patch('oslo_log.log.setup')
def test_main(self, log_setup, monkey_patch, service_create,
get_launcher):
CONF.set_override('enabled_backends', None)
self.assertRaises(SystemExit, cinder_volume.main)
self.assertFalse(service_create.called)
self.assertFalse(self._mock_win32_proc_launcher.called)
@mock.patch('cinder.service.get_launcher')
@mock.patch('cinder.service.Service.create')
@mock.patch('cinder.utils.monkey_patch')
@mock.patch('oslo_log.log.setup')
def test_main_invalid_backend(self, log_setup, monkey_patch,
service_create, get_launcher):
CONF.set_override('enabled_backends', 'backend1')
CONF.set_override('backend_name', 'backend2')
self.assertRaises(exception.InvalidInput, cinder_volume.main)
self.assertFalse(service_create.called)
self.assertFalse(self._mock_win32_proc_launcher.called)
@mock.patch('cinder.utils.monkey_patch')
@mock.patch('oslo_log.log.setup')
@ddt.data({},
{'binary_path': 'cinder-volume-script.py',
'exp_py_executable': True})
@ddt.unpack
def test_main_with_multiple_backends(self, log_setup, monkey_patch,
binary_path='cinder-volume',
exp_py_executable=False):
# If multiple backends are used, we expect the Windows process
# launcher to be used in order to create the child processes.
backends = ['', 'backend1', 'backend2', '']
CONF.set_override('enabled_backends', backends)
CONF.set_override('host', 'host')
launcher = self._mock_win32_proc_launcher
# Depending on the setuptools version, '-script.py' and '.exe'
# binary path extensions may be trimmed. We need to take this
# into consideration when building the command that will be
# used to spawn child subprocesses.
sys.argv = [binary_path]
cinder_volume.main()
self.assertEqual('cinder', CONF.project)
self.assertEqual(CONF.version, version.version_string())
log_setup.assert_called_once_with(CONF, "cinder")
monkey_patch.assert_called_once_with()
exp_cmd_prefix = [sys.executable] if exp_py_executable else []
exp_cmds = [
exp_cmd_prefix + sys.argv + ['--backend_name=%s' % backend_name]
for backend_name in ['backend1', 'backend2']]
launcher.add_process.assert_has_calls(
[mock.call(exp_cmd) for exp_cmd in exp_cmds])
launcher.wait.assert_called_once_with()
@mock.patch('cinder.service.get_launcher')
@mock.patch('cinder.service.Service.create')
@mock.patch('cinder.utils.monkey_patch')
@mock.patch('oslo_log.log.setup')
def test_main_with_multiple_backends_child(
self, log_setup, monkey_patch, service_create, get_launcher):
# We're testing the code expected to be run within child processes.
backends = ['', 'backend1', 'backend2', '']
CONF.set_override('enabled_backends', backends)
CONF.set_override('host', 'host')
CONF.set_override('cluster', None)
launcher = get_launcher.return_value
sys.argv += ['--backend_name', 'backend2']
cinder_volume.main()
self.assertEqual('cinder', CONF.project)
self.assertEqual(CONF.version, version.version_string())
log_setup.assert_called_once_with(CONF, "cinder")
monkey_patch.assert_called_once_with()
service_create.assert_called_once_with(
binary=constants.VOLUME_BINARY, host='host@backend2',
service_name='backend2', coordination=True,
cluster=None)
launcher.launch_service.assert_called_once_with(
service_create.return_value)
@mock.patch('cinder.service.get_launcher')
@mock.patch('cinder.service.Service.create')
@mock.patch('cinder.utils.monkey_patch')
@mock.patch('oslo_log.log.setup')
def test_main_with_single_backend(
self, log_setup, monkey_patch, service_create, get_launcher):
# We're expecting the service to be run within the same process.
CONF.set_override('enabled_backends', ['backend2'])
CONF.set_override('host', 'host')
CONF.set_override('cluster', None)
launcher = get_launcher.return_value
cinder_volume.main()
self.assertEqual('cinder', CONF.project)
self.assertEqual(CONF.version, version.version_string())
log_setup.assert_called_once_with(CONF, "cinder")
monkey_patch.assert_called_once_with()
service_create.assert_called_once_with(
binary=constants.VOLUME_BINARY, host='host@backend2',
service_name='backend2', coordination=True,
cluster=None)
launcher.launch_service.assert_called_once_with(
service_create.return_value)
@ddt.ddt
class TestCinderManageCmd(test.TestCase):
+3 -131
View File
@@ -31,7 +31,6 @@ from cinder.volume import throttling
class TestQemuImgInfo(test.TestCase):
@mock.patch('cinder.privsep.format_inspector.get_format_if_safe')
@mock.patch('os.name', new='posix')
@mock.patch('oslo_utils.imageutils.QemuImgInfo')
@mock.patch('cinder.utils.execute')
def test_qemu_img_info(self, mock_exec, mock_info, mock_detect):
@@ -52,7 +51,6 @@ class TestQemuImgInfo(test.TestCase):
allow_qcow2_backing_file=False)
@mock.patch('cinder.privsep.format_inspector.get_format_if_safe')
@mock.patch('os.name', new='posix')
@mock.patch('oslo_utils.imageutils.QemuImgInfo')
@mock.patch('cinder.utils.execute')
def test_qemu_img_info_qcow2_backing_ok(
@@ -75,7 +73,6 @@ class TestQemuImgInfo(test.TestCase):
allow_qcow2_backing_file=True)
@mock.patch('cinder.privsep.format_inspector.get_format_if_safe')
@mock.patch('os.name', new='posix')
@mock.patch('oslo_utils.imageutils.QemuImgInfo')
@mock.patch('cinder.utils.execute')
def test_qemu_img_info_raw_not_luks(self, mock_exec, mock_info,
@@ -111,7 +108,6 @@ class TestQemuImgInfo(test.TestCase):
allow_qcow2_backing_file=False)
@mock.patch('cinder.privsep.format_inspector.get_format_if_safe')
@mock.patch('os.name', new='posix')
@mock.patch('oslo_utils.imageutils.QemuImgInfo')
@mock.patch('cinder.utils.execute')
def test_qemu_img_info_luks(self, mock_exec, mock_info, mock_detect):
@@ -147,7 +143,6 @@ class TestQemuImgInfo(test.TestCase):
allow_qcow2_backing_file=False)
@mock.patch('cinder.privsep.format_inspector.get_format_if_safe')
@mock.patch('os.name', new='posix')
@mock.patch('oslo_utils.imageutils.QemuImgInfo')
@mock.patch('cinder.utils.execute')
def test_qemu_img_info_not_root(self, mock_exec, mock_info, mock_detect):
@@ -170,29 +165,6 @@ class TestQemuImgInfo(test.TestCase):
allow_qcow2_backing_file=False)
@mock.patch('cinder.privsep.format_inspector.get_format_if_safe')
@mock.patch('cinder.image.image_utils.os')
@mock.patch('oslo_utils.imageutils.QemuImgInfo')
@mock.patch('cinder.utils.execute')
def test_qemu_img_info_on_nt(self, mock_exec, mock_info, mock_os,
mock_detect):
mock_out = mock.sentinel.out
mock_err = mock.sentinel.err
test_path = mock.sentinel.path
mock_exec.return_value = (mock_out, mock_err)
mock_os.name = 'nt'
mock_detect.return_value = 'mock_fmt'
output = image_utils.qemu_img_info(test_path)
mock_exec.assert_called_once_with(
'qemu-img', 'info', '-f', 'mock_fmt', '--output=json',
test_path, run_as_root=True, prlimit=image_utils.QEMU_IMG_LIMITS)
self.assertEqual(mock_info.return_value, output)
mock_detect.assert_called_once_with(path=test_path,
allow_qcow2_backing_file=False)
@mock.patch('cinder.privsep.format_inspector.get_format_if_safe')
@mock.patch('os.name', new='posix')
@mock.patch('cinder.utils.execute')
def test_qemu_img_info_malicious(self, mock_exec, mock_detect):
mock_out = mock.sentinel.out
@@ -961,8 +933,7 @@ class TestUploadVolume(test.TestCase):
@mock.patch('cinder.image.image_utils.qemu_img_info')
@mock.patch('cinder.image.image_utils.convert_image')
@mock.patch('cinder.image.image_utils.temporary_file')
@mock.patch('cinder.image.image_utils.os')
def test_diff_format(self, image_format, mock_os, mock_temp, mock_convert,
def test_diff_format(self, image_format, mock_temp, mock_convert,
mock_info, mock_open, mock_proxy):
input_format, output_format, do_compress = image_format
ctxt = mock.sentinel.context
@@ -971,7 +942,6 @@ class TestUploadVolume(test.TestCase):
'disk_format': input_format,
'container_format': mock.sentinel.container_format}
volume_path = mock.sentinel.volume_path
mock_os.name = 'posix'
data = mock_info.return_value
data.file_format = output_format
data.backing_file = None
@@ -1012,7 +982,6 @@ class TestUploadVolume(test.TestCase):
'disk_format': 'raw',
'container_format': mock.sentinel.container_format}
volume_path = mock.sentinel.volume_path
mock_os.name = 'posix'
mock_os.access.return_value = False
output = image_utils.upload_volume(ctxt, image_service, image_meta,
@@ -1043,7 +1012,6 @@ class TestUploadVolume(test.TestCase):
image_meta = {'id': 'test_id',
'disk_format': 'raw',
'container_format': mock.sentinel.container_format}
mock_os.name = 'posix'
mock_os.access.return_value = False
output = image_utils.upload_volume(ctxt, image_service, image_meta,
@@ -1069,8 +1037,7 @@ class TestUploadVolume(test.TestCase):
@mock.patch('cinder.image.image_utils.qemu_img_info')
@mock.patch('cinder.image.image_utils.convert_image')
@mock.patch('cinder.image.image_utils.temporary_file')
@mock.patch('cinder.image.image_utils.os')
def test_same_format_compressed(self, mock_os, mock_temp, mock_convert,
def test_same_format_compressed(self, mock_temp, mock_convert,
mock_info, mock_open,
mock_chown, mock_proxy,
mock_engine_ready, mock_get_engine):
@@ -1089,98 +1056,6 @@ class TestUploadVolume(test.TestCase):
'container_format': 'compressed'}
self.flags(allow_compression_on_image_upload=True)
volume_path = mock.sentinel.volume_path
mock_os.name = 'posix'
data = mock_info.return_value
data.file_format = 'raw'
data.backing_file = None
temp_file = mock_temp.return_value.__enter__.return_value
mock_engine = mock.Mock(spec=fakeEngine)
mock_get_engine.return_value = mock_engine
output = image_utils.upload_volume(ctxt, image_service, image_meta,
volume_path)
self.assertIsNone(output)
mock_convert.assert_called_once_with(volume_path,
temp_file,
'raw',
compress=True,
run_as_root=True,
image_id=image_meta['id'],
data=data)
mock_info.assert_called_with(temp_file, run_as_root=True)
self.assertEqual(2, mock_info.call_count)
mock_open.assert_called_once_with(temp_file, 'rb')
mock_proxy.assert_called_once_with(
mock_open.return_value.__enter__.return_value)
image_service.update.assert_called_once_with(
ctxt, image_meta['id'], {}, mock_proxy.return_value,
store_id=None, base_image_ref=None)
mock_engine.compress_img.assert_called()
@mock.patch('eventlet.tpool.Proxy')
@mock.patch('cinder.image.image_utils.utils.temporary_chown')
@mock.patch('cinder.image.image_utils.open', new_callable=mock.mock_open)
@mock.patch('cinder.image.image_utils.qemu_img_info')
@mock.patch('cinder.image.image_utils.convert_image')
@mock.patch('cinder.image.image_utils.temporary_file')
@mock.patch('cinder.image.image_utils.os')
def test_same_format_on_nt(self, mock_os, mock_temp, mock_convert,
mock_info, mock_open, mock_chown,
mock_proxy):
ctxt = mock.sentinel.context
image_service = mock.Mock()
image_meta = {'id': 'test_id',
'disk_format': 'raw',
'container_format': 'bare'}
volume_path = mock.sentinel.volume_path
mock_os.name = 'nt'
mock_os.access.return_value = False
output = image_utils.upload_volume(ctxt, image_service, image_meta,
volume_path)
self.assertIsNone(output)
self.assertFalse(mock_convert.called)
self.assertFalse(mock_info.called)
mock_open.assert_called_once_with(volume_path, 'rb')
mock_proxy.assert_called_once_with(
mock_open.return_value.__enter__.return_value)
image_service.update.assert_called_once_with(
ctxt, image_meta['id'], {}, mock_proxy.return_value,
store_id=None, base_image_ref=None)
@mock.patch('cinder.image.accelerator.ImageAccel._get_engine')
@mock.patch('cinder.image.accelerator.ImageAccel.is_engine_ready',
return_value = True)
@mock.patch('eventlet.tpool.Proxy')
@mock.patch('cinder.image.image_utils.utils.temporary_chown')
@mock.patch('cinder.image.image_utils.open', new_callable=mock.mock_open)
@mock.patch('cinder.image.image_utils.qemu_img_info')
@mock.patch('cinder.image.image_utils.convert_image')
@mock.patch('cinder.image.image_utils.temporary_file')
@mock.patch('cinder.image.image_utils.os')
def test_same_format_on_nt_compressed(self, mock_os, mock_temp,
mock_convert, mock_info,
mock_open,
mock_chown, mock_proxy,
mock_engine_ready, mock_get_engine):
class fakeEngine(object):
def __init__(self):
pass
def compress_img(self, src, dest, run_as_root):
pass
ctxt = mock.sentinel.context
image_service = mock.Mock()
image_meta = {'id': 'test_id',
'disk_format': 'raw',
'container_format': 'compressed'}
self.flags(allow_compression_on_image_upload=True)
volume_path = mock.sentinel.volume_path
mock_os.name = 'posix'
data = mock_info.return_value
data.file_format = 'raw'
data.backing_file = None
@@ -1212,15 +1087,13 @@ class TestUploadVolume(test.TestCase):
@mock.patch('cinder.image.image_utils.qemu_img_info')
@mock.patch('cinder.image.image_utils.convert_image')
@mock.patch('cinder.image.image_utils.temporary_file')
@mock.patch('cinder.image.image_utils.os')
def test_convert_error(self, mock_os, mock_temp, mock_convert, mock_info):
def test_convert_error(self, mock_temp, mock_convert, mock_info):
ctxt = mock.sentinel.context
image_service = mock.Mock()
image_meta = {'id': 'test_id',
'disk_format': mock.sentinel.disk_format,
'container_format': mock.sentinel.container_format}
volume_path = mock.sentinel.volume_path
mock_os.name = 'posix'
data = mock_info.return_value
data.file_format = mock.sentinel.other_disk_format
data.backing_file = None
@@ -1255,7 +1128,6 @@ class TestUploadVolume(test.TestCase):
'disk_format': 'raw',
'container_format': mock.sentinel.container_format}
volume_path = mock.sentinel.volume_path
mock_os.name = 'posix'
mock_os.access.return_value = False
image_utils.upload_volume(ctxt, image_service, image_meta,
-80
View File
@@ -553,83 +553,3 @@ class TestWSGIService(test.TestCase):
use_ssl=True)
self.assertTrue(mock_loader.called)
class OSCompatibilityTestCase(test.TestCase):
def _test_service_launcher(self, fake_os):
# Note(lpetrut): The cinder-volume service needs to be spawned
# differently on Windows due to an eventlet bug. For this reason,
# we must check the process launcher used.
fake_process_launcher = mock.MagicMock()
with mock.patch('os.name', fake_os):
with mock.patch('cinder.service.process_launcher',
fake_process_launcher):
launcher = service.get_launcher()
if fake_os == 'nt':
self.assertEqual(service.Launcher, type(launcher))
else:
self.assertEqual(fake_process_launcher(), launcher)
def test_process_launcher_on_windows(self):
self._test_service_launcher('nt')
def test_process_launcher_on_linux(self):
self._test_service_launcher('posix')
class WindowsProcessLauncherTestCase(test.TestCase):
@mock.patch.object(service, 'os_win_utilsfactory', create=True)
@mock.patch('oslo_service.service.SignalHandler')
def setUp(self, mock_signal_handler_cls, mock_utilsfactory):
super(WindowsProcessLauncherTestCase, self).setUp()
self._signal_handler = mock_signal_handler_cls.return_value
self._processutils = mock_utilsfactory.get_processutils.return_value
self._launcher = service.WindowsProcessLauncher()
def test_setup_signal_handlers(self):
exp_signal_map = {'SIGINT': self._launcher._terminate,
'SIGTERM': self._launcher._terminate}
self._signal_handler.add_handler.assert_has_calls(
[mock.call(signal, handler)
for signal, handler in exp_signal_map.items()],
any_order=True)
@mock.patch('sys.exit')
def test_terminate_handler(self, mock_exit):
self._launcher._terminate(mock.sentinel.signum, mock.sentinel.frame)
mock_exit.assert_called_once_with(1)
@mock.patch('subprocess.Popen')
def test_launch(self, mock_popen):
mock_workers = [mock.Mock(), mock.Mock(), mock.Mock()]
mock_popen.side_effect = mock_workers
self._processutils.kill_process_on_job_close.side_effect = [
exception.CinderException, None, None]
# We expect the first process to be cleaned up after failing
# to setup a job object.
self.assertRaises(exception.CinderException,
self._launcher.add_process,
mock.sentinel.cmd1)
mock_workers[0].kill.assert_called_once_with()
self._launcher.add_process(mock.sentinel.cmd2)
self._launcher.add_process(mock.sentinel.cmd3)
mock_popen.assert_has_calls(
[mock.call(cmd)
for cmd in [mock.sentinel.cmd1,
mock.sentinel.cmd2,
mock.sentinel.cmd3]])
self._processutils.kill_process_on_job_close.assert_has_calls(
[mock.call(worker.pid) for worker in mock_workers[1:]])
self._launcher.wait()
wait_processes = self._processutils.wait_for_multiple_processes
wait_processes.assert_called_once_with(
[worker.pid for worker in mock_workers[1:]],
wait_all=True)
-10
View File
@@ -323,16 +323,6 @@ class TemporaryChownTestCase(test.TestCase):
mock_stat.assert_called_once_with(test_filename)
self.assertFalse(mock_exec.called)
@mock.patch('os.name', 'nt')
@mock.patch('os.stat')
@mock.patch('cinder.utils.execute')
def test_temporary_chown_win32(self, mock_exec, mock_stat):
with utils.temporary_chown(mock.sentinel.path):
pass
mock_exec.assert_not_called()
mock_stat.assert_not_called()
class TempdirTestCase(test.TestCase):
@mock.patch('tempfile.mkdtemp')
@@ -1106,8 +1106,7 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
mock_qemu_img_info.assert_called_once_with(self._fake_snapshot_path)
@ddt.data({},
{'info_file_exists': True},
{'os_name': 'nt'})
{'info_file_exists': True})
@ddt.unpack
@mock.patch('json.dump')
@mock.patch('cinder.volume.drivers.remotefs.open')
@@ -1116,8 +1115,7 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
mock_os_path_exists,
mock_open,
mock_json_dump,
info_file_exists=False,
os_name='posix'):
info_file_exists=False):
mock_os_path_exists.return_value = info_file_exists
fake_info_path = '/path/to/info'
@@ -1131,7 +1129,7 @@ class RemoteFsSnapDriverTestCase(test.TestCase):
mock_json_dump.assert_called_once_with(
fake_snapshot_info, mock.ANY, indent=1, sort_keys=True)
if info_file_exists or os.name == 'nt':
if info_file_exists:
self._driver._execute.assert_not_called()
self._driver._set_rw_permissions.assert_not_called()
else:
@@ -1390,35 +1388,6 @@ class RemoteFSManageableVolumesTestCase(test.TestCase):
}
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')
-48
View File
@@ -1,48 +0,0 @@
# Copyright 2012 Pedro Navarro Perez
#
# 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.
"""
Stubouts, mocks and fixtures for windows volume test suite
"""
def get_fake_volume_info():
return {'name': 'volume_name',
'size': 1,
'provider_location': 'iqn.2010-10.org.openstack:' + 'volume_name',
'id': 1,
'provider_auth': None}
def get_fake_volume_info_cloned():
return {'name': 'volume_name_cloned',
'size': 1,
'provider_location': 'iqn.2010-10.org.openstack:' +
'volume_name_cloned',
'id': 1,
'provider_auth': None}
def get_fake_image_meta():
return {'id': '10958016-e196-42e3-9e7f-5d8927ae3099'
}
def get_fake_snapshot_info():
return {'name': 'snapshot_name',
'volume_name': 'volume_name', }
def get_fake_connector_info():
return {'initiator': 'iqn.2010-10.org.openstack:' + 'volume_name', }
-501
View File
@@ -1,501 +0,0 @@
# Copyright 2012 Pedro Navarro Perez
# Copyright 2015 Cloudbase Solutions SRL
# All Rights Reserved.
#
# 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.
"""
Unit tests for Windows Server 2012 OpenStack Cinder volume driver
"""
import os
from unittest import mock
import ddt
from oslo_utils import fileutils
from oslo_utils import timeutils
from oslo_utils import units
from cinder import context
from cinder import exception
from cinder.image import image_utils
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.tests.unit import test
from cinder.tests.unit import utils as test_utils
from cinder.tests.unit.windows import db_fakes
from cinder.volume import configuration as conf
from cinder.volume.drivers.windows import iscsi as windows_iscsi
@ddt.ddt
class TestWindowsISCSIDriver(test.TestCase):
@mock.patch.object(windows_iscsi, 'utilsfactory')
def setUp(self, mock_utilsfactory):
super(TestWindowsISCSIDriver, self).setUp()
self.configuration = conf.Configuration(None)
self.configuration.append_config_values(windows_iscsi.windows_opts)
self.flags(windows_iscsi_lun_path='fake_iscsi_lun_path')
self.flags(image_conversion_dir='fake_image_conversion_dir')
self._driver = windows_iscsi.WindowsISCSIDriver(
configuration=self.configuration)
self._context = context.get_admin_context()
self.updated_at = timeutils.utcnow()
@mock.patch.object(fileutils, 'ensure_tree')
def test_do_setup(self, mock_ensure_tree):
self._driver.do_setup(mock.sentinel.context)
mock_ensure_tree.assert_has_calls(
[mock.call('fake_iscsi_lun_path'),
mock.call('fake_image_conversion_dir')])
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_portals')
def test_check_for_setup_error(self, mock_get_portals):
self._driver.check_for_setup_error()
mock_get_portals.assert_called_once_with()
@ddt.data(True, False)
def test_get_portals(self, portals_available=True):
iscsi_port = mock.sentinel.iscsi_port
available_ips = ['fake_ip0', 'fake_ip1', 'fake_unrequested_ip']
requested_ips = available_ips[:-1] + ['fake_inexistent_ips']
available_portals = ([":".join([ip_addr, str(iscsi_port)])
for ip_addr in available_ips]
if portals_available else [])
self._driver.configuration = mock.Mock()
self._driver.configuration.target_port = iscsi_port
self._driver.configuration.target_ip_address = requested_ips[0]
self._driver.configuration.target_secondary_ip_addresses = (
requested_ips[1:])
self._driver._tgt_utils.get_portal_locations.return_value = (
available_portals)
if portals_available:
portals = self._driver._get_portals()
self.assertEqual(set(available_portals[:-1]), set(portals))
else:
self.assertRaises(exception.VolumeDriverException,
self._driver._get_portals)
self._driver._tgt_utils.get_portal_locations.assert_called_once_with(
available_only=True,
fail_if_none_found=True)
@ddt.data(True, False)
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_portals')
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_target_name')
def test_get_host_information(self, multipath, mock_get_target_name,
mock_get_portals):
tgt_utils = self._driver._tgt_utils
fake_auth_meth = 'CHAP'
fake_chap_username = 'fake_chap_username'
fake_chap_password = 'fake_chap_password'
fake_target_iqn = 'fake_target_iqn'
fake_host_info = {'target_iqn': 'fake_target_iqn',
'fake_prop': 'fake_value'}
fake_provider_auth = "%s %s %s" % (fake_auth_meth,
fake_chap_username,
fake_chap_password)
fake_portals = [mock.sentinel.portal_location0,
mock.sentinel.portal_location1]
volume = fake_volume.fake_volume_obj(mock.sentinel.context,
provider_auth=fake_provider_auth)
mock_get_target_name.return_value = mock.sentinel.target_name
mock_get_portals.return_value = fake_portals
tgt_utils.get_target_information.return_value = fake_host_info
expected_host_info = dict(fake_host_info,
auth_method=fake_auth_meth,
auth_username=fake_chap_username,
auth_password=fake_chap_password,
target_discovered=False,
target_portal=fake_portals[0],
target_lun=0,
volume_id=volume.id)
if multipath:
expected_host_info['target_portals'] = fake_portals
expected_host_info['target_iqns'] = [fake_target_iqn] * 2
expected_host_info['target_luns'] = [0] * 2
host_info = self._driver._get_host_information(volume, multipath)
self.assertEqual(expected_host_info, host_info)
mock_get_target_name.assert_called_once_with(volume)
mock_get_portals.assert_called_once_with()
tgt_utils.get_target_information.assert_called_once_with(
mock.sentinel.target_name)
@mock.patch.object(windows_iscsi.WindowsISCSIDriver,
'_get_host_information')
def test_initialize_connection(self, mock_get_host_info):
tgt_utils = self._driver._tgt_utils
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
fake_initiator = db_fakes.get_fake_connector_info()
fake_initiator['multipath'] = mock.sentinel.multipath
fake_host_info = {'fake_host_prop': 'fake_value'}
mock_get_host_info.return_value = fake_host_info
expected_conn_info = {'driver_volume_type': 'iscsi',
'data': fake_host_info}
conn_info = self._driver.initialize_connection(volume,
fake_initiator)
self.assertEqual(expected_conn_info, conn_info)
mock_get_host_info.assert_called_once_with(
volume, mock.sentinel.multipath)
mock_associate = tgt_utils.associate_initiator_with_iscsi_target
mock_associate.assert_called_once_with(
fake_initiator['initiator'],
volume.provider_location)
def test_terminate_connection(self):
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
fake_initiator = db_fakes.get_fake_connector_info()
self._driver.terminate_connection(volume, fake_initiator)
self._driver._tgt_utils.deassociate_initiator.assert_called_once_with(
fake_initiator['initiator'], volume.provider_location)
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, 'local_path')
def test_create_volume(self, mock_local_path):
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
self._driver.create_volume(volume)
mock_local_path.assert_called_once_with(volume)
self._driver._tgt_utils.create_wt_disk.assert_called_once_with(
mock_local_path.return_value,
volume.name,
size_mb=volume.size * 1024)
def test_local_path(self):
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
fake_lun_path = 'fake_lun_path'
self.flags(windows_iscsi_lun_path=fake_lun_path)
disk_format = 'vhd'
mock_get_fmt = self._driver._tgt_utils.get_supported_disk_format
mock_get_fmt.return_value = disk_format
disk_path = self._driver.local_path(volume)
expected_fname = "%s.%s" % (volume.name, disk_format)
expected_disk_path = os.path.join(fake_lun_path,
expected_fname)
self.assertEqual(expected_disk_path, disk_path)
mock_get_fmt.assert_called_once_with()
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, 'local_path')
@mock.patch.object(fileutils, 'delete_if_exists')
def test_delete_volume(self, mock_delete_if_exists, mock_local_path):
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
self._driver.delete_volume(volume)
mock_local_path.assert_called_once_with(volume)
self._driver._tgt_utils.remove_wt_disk.assert_called_once_with(
volume.name)
mock_delete_if_exists.assert_called_once_with(
mock_local_path.return_value)
def test_create_snapshot(self):
volume = fake_volume.fake_volume_obj(context.get_admin_context())
snapshot = fake_snapshot.fake_snapshot_obj(context.get_admin_context(),
volume_id=volume.id)
snapshot.volume = volume
self._driver.create_snapshot(snapshot)
self._driver._tgt_utils.create_snapshot.assert_called_once_with(
snapshot.volume_name, snapshot.name)
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, 'local_path')
def test_create_volume_from_snapshot(self, mock_local_path):
volume = fake_volume.fake_volume_obj(context.get_admin_context())
snapshot = fake_snapshot.fake_snapshot_obj(context.get_admin_context())
snapshot.volume = volume
self._driver.create_volume_from_snapshot(volume, snapshot)
self._driver._tgt_utils.export_snapshot.assert_called_once_with(
snapshot.name, mock_local_path.return_value)
self._driver._tgt_utils.import_wt_disk.assert_called_once_with(
mock_local_path.return_value, volume.name)
def test_delete_snapshot(self):
snapshot = fake_snapshot.fake_snapshot_obj(context.get_admin_context())
self._driver.delete_snapshot(snapshot)
self._driver._tgt_utils.delete_snapshot.assert_called_once_with(
snapshot.name)
def test_get_target_name(self):
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
expected_target_name = "%s%s" % (
self._driver.configuration.target_prefix,
volume.name)
target_name = self._driver._get_target_name(volume)
self.assertEqual(expected_target_name, target_name)
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_target_name')
@mock.patch.object(windows_iscsi.volume_utils, 'generate_username')
@mock.patch.object(windows_iscsi.volume_utils, 'generate_password')
def test_create_export(self, mock_generate_password,
mock_generate_username,
mock_get_target_name):
tgt_utils = self._driver._tgt_utils
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
self._driver.configuration.chap_username = None
self._driver.configuration.chap_password = None
self._driver.configuration.use_chap_auth = True
fake_chap_username = 'fake_chap_username'
fake_chap_password = 'fake_chap_password'
mock_get_target_name.return_value = mock.sentinel.target_name
mock_generate_username.return_value = fake_chap_username
mock_generate_password.return_value = fake_chap_password
tgt_utils.iscsi_target_exists.return_value = False
vol_updates = self._driver.create_export(mock.sentinel.context,
volume,
mock.sentinel.connector)
mock_get_target_name.assert_called_once_with(volume)
tgt_utils.iscsi_target_exists.assert_called_once_with(
mock.sentinel.target_name)
tgt_utils.set_chap_credentials.assert_called_once_with(
mock.sentinel.target_name,
fake_chap_username,
fake_chap_password)
tgt_utils.add_disk_to_target.assert_called_once_with(
volume.name, mock.sentinel.target_name)
expected_provider_auth = ' '.join(('CHAP',
fake_chap_username,
fake_chap_password))
expected_vol_updates = dict(
provider_location=mock.sentinel.target_name,
provider_auth=expected_provider_auth)
self.assertEqual(expected_vol_updates, vol_updates)
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_target_name')
def test_remove_export(self, mock_get_target_name):
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
self._driver.remove_export(mock.sentinel.context, volume)
mock_get_target_name.assert_called_once_with(volume)
self._driver._tgt_utils.delete_iscsi_target.assert_called_once_with(
mock_get_target_name.return_value)
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, 'local_path')
@mock.patch.object(image_utils, 'temporary_file')
@mock.patch.object(image_utils, 'fetch_to_vhd')
@mock.patch('os.unlink')
def test_copy_image_to_volume(self, mock_unlink, mock_fetch_to_vhd,
mock_tmp_file, mock_local_path):
tgt_utils = self._driver._tgt_utils
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
mock_tmp_file.return_value.__enter__.return_value = (
mock.sentinel.tmp_vhd_path)
mock_local_path.return_value = mock.sentinel.vol_vhd_path
self._driver.copy_image_to_volume(mock.sentinel.context,
volume,
mock.sentinel.image_service,
mock.sentinel.image_id)
mock_local_path.assert_called_once_with(volume)
mock_tmp_file.assert_called_once_with(suffix='.vhd')
image_utils.fetch_to_vhd.assert_called_once_with(
mock.sentinel.context, mock.sentinel.image_service,
mock.sentinel.image_id, mock.sentinel.tmp_vhd_path,
self._driver.configuration.volume_dd_blocksize,
disable_sparse=False)
mock_unlink.assert_called_once_with(mock.sentinel.vol_vhd_path)
self._driver._vhdutils.convert_vhd.assert_called_once_with(
mock.sentinel.tmp_vhd_path,
mock.sentinel.vol_vhd_path,
tgt_utils.get_supported_vhd_type.return_value)
self._driver._vhdutils.resize_vhd.assert_called_once_with(
mock.sentinel.vol_vhd_path,
volume.size * units.Gi,
is_file_max_size=False)
tgt_utils.change_wt_disk_status.assert_has_calls(
[mock.call(volume.name, enabled=False),
mock.call(volume.name, enabled=True)])
@mock.patch.object(windows_iscsi.uuidutils, 'generate_uuid')
def test_temporary_snapshot(self, mock_generate_uuid):
tgt_utils = self._driver._tgt_utils
mock_generate_uuid.return_value = mock.sentinel.snap_uuid
expected_snap_name = '%s-tmp-snapshot-%s' % (
mock.sentinel.volume_name, mock.sentinel.snap_uuid)
with self._driver._temporary_snapshot(
mock.sentinel.volume_name) as snap_name:
self.assertEqual(expected_snap_name, snap_name)
tgt_utils.create_snapshot.assert_called_once_with(
mock.sentinel.volume_name, expected_snap_name)
tgt_utils.delete_snapshot.assert_called_once_with(
expected_snap_name)
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_temporary_snapshot')
@mock.patch.object(image_utils, 'upload_volume')
@mock.patch.object(fileutils, 'delete_if_exists')
def test_copy_volume_to_image(self, mock_delete_if_exists,
mock_upload_volume,
mock_tmp_snap):
tgt_utils = self._driver._tgt_utils
disk_format = 'vhd'
fake_image_meta = db_fakes.get_fake_image_meta()
fake_volume = test_utils.create_volume(
self._context, volume_type_id=fake.VOLUME_TYPE_ID,
updated_at=self.updated_at)
extra_specs = {
'image_service:store_id': 'fake-store'
}
test_utils.create_volume_type(self._context.elevated(),
id=fake.VOLUME_TYPE_ID, name="test_type",
extra_specs=extra_specs)
fake_img_conv_dir = 'fake_img_conv_dir'
self.flags(image_conversion_dir=fake_img_conv_dir)
tgt_utils.get_supported_disk_format.return_value = disk_format
mock_tmp_snap.return_value.__enter__.return_value = (
mock.sentinel.tmp_snap_name)
expected_tmp_vhd_path = os.path.join(
fake_img_conv_dir,
fake_image_meta['id'] + '.' + disk_format)
self._driver.copy_volume_to_image(
mock.sentinel.context, fake_volume,
mock.sentinel.image_service,
fake_image_meta)
mock_tmp_snap.assert_called_once_with(fake_volume.name)
tgt_utils.export_snapshot.assert_called_once_with(
mock.sentinel.tmp_snap_name,
expected_tmp_vhd_path)
mock_upload_volume.assert_called_once_with(
mock.sentinel.context, mock.sentinel.image_service,
fake_image_meta, expected_tmp_vhd_path, volume_fd=None,
volume_format='vhd',
store_id='fake-store', base_image_ref=None,
compress=True, run_as_root=True)
mock_delete_if_exists.assert_called_once_with(
expected_tmp_vhd_path)
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_temporary_snapshot')
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, 'local_path')
def test_create_cloned_volume(self, mock_local_path,
mock_tmp_snap):
tgt_utils = self._driver._tgt_utils
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
src_volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
mock_tmp_snap.return_value.__enter__.return_value = (
mock.sentinel.tmp_snap_name)
mock_local_path.return_value = mock.sentinel.vol_vhd_path
self._driver.create_cloned_volume(volume, src_volume)
mock_tmp_snap.assert_called_once_with(src_volume.name)
tgt_utils.export_snapshot.assert_called_once_with(
mock.sentinel.tmp_snap_name,
mock.sentinel.vol_vhd_path)
self._driver._vhdutils.resize_vhd.assert_called_once_with(
mock.sentinel.vol_vhd_path, volume.size * units.Gi,
is_file_max_size=False)
tgt_utils.import_wt_disk.assert_called_once_with(
mock.sentinel.vol_vhd_path, volume.name)
@mock.patch('os.path.splitdrive')
def test_get_capacity_info(self, mock_splitdrive):
mock_splitdrive.return_value = (mock.sentinel.drive,
mock.sentinel.path_tail)
fake_size_gb = 2
fake_free_space_gb = 1
self._driver._hostutils.get_volume_info.return_value = (
fake_size_gb * units.Gi,
fake_free_space_gb * units.Gi)
total_gb, free_gb = self._driver._get_capacity_info()
self.assertEqual(fake_size_gb, total_gb)
self.assertEqual(fake_free_space_gb, free_gb)
self._driver._hostutils.get_volume_info.assert_called_once_with(
mock.sentinel.drive)
mock_splitdrive.assert_called_once_with('fake_iscsi_lun_path')
@mock.patch.object(windows_iscsi.WindowsISCSIDriver, '_get_capacity_info')
def test_update_volume_stats(self, mock_get_capacity_info):
mock_get_capacity_info.return_value = (
mock.sentinel.size_gb,
mock.sentinel.free_space_gb)
self.flags(volume_backend_name='volume_backend_name')
self.flags(reserved_percentage=10)
expected_volume_stats = dict(
volume_backend_name='volume_backend_name',
vendor_name='Microsoft',
driver_version=self._driver.VERSION,
storage_protocol='iSCSI',
total_capacity_gb=mock.sentinel.size_gb,
free_capacity_gb=mock.sentinel.free_space_gb,
reserved_percentage=10,
QoS_support=False)
self._driver._update_volume_stats()
self.assertEqual(expected_volume_stats,
self._driver._stats)
def test_extend_volume(self):
volume = fake_volume.fake_volume_obj(mock.sentinel.fake_context)
new_size_gb = 2
expected_additional_sz_mb = 1024
self._driver.extend_volume(volume, new_size_gb)
self._driver._tgt_utils.extend_wt_disk.assert_called_once_with(
volume.name, expected_additional_sz_mb)
-956
View File
@@ -1,956 +0,0 @@
# Copyright 2014 Cloudbase Solutions Srl
#
# 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 copy
import os
from unittest import mock
import ddt
from oslo_utils import timeutils
from oslo_utils import units
from cinder import context
from cinder import exception
from cinder.image import image_utils
from cinder.objects import fields
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_volume
from cinder.tests.unit import test
from cinder.tests.unit import utils as test_utils
from cinder.volume.drivers import remotefs
from cinder.volume.drivers.windows import smbfs
@ddt.ddt
class WindowsSmbFsTestCase(test.TestCase):
_FAKE_SHARE = '//1.2.3.4/share1'
_FAKE_SHARE_HASH = 'db0bf952c1734092b83e8990bd321131'
_FAKE_MNT_BASE = r'c:\openstack\mnt'
_FAKE_MNT_POINT = os.path.join(_FAKE_MNT_BASE, _FAKE_SHARE_HASH)
_FAKE_VOLUME_ID = '4f711859-4928-4cb7-801a-a50c37ceaccc'
_FAKE_VOLUME_NAME = 'volume-%s.vhdx' % _FAKE_VOLUME_ID
_FAKE_SNAPSHOT_ID = '50811859-4928-4cb7-801a-a50c37ceacba'
_FAKE_SNAPSHOT_NAME = 'volume-%s-%s.vhdx' % (_FAKE_VOLUME_ID,
_FAKE_SNAPSHOT_ID)
_FAKE_SNAPSHOT_PATH = os.path.join(_FAKE_MNT_POINT,
_FAKE_SNAPSHOT_NAME)
_FAKE_VOLUME_SIZE = 1
_FAKE_TOTAL_SIZE = 2048
_FAKE_TOTAL_AVAILABLE = 1024
_FAKE_TOTAL_ALLOCATED = 1024
_FAKE_SHARE_OPTS = '-o username=Administrator,password=12345'
_FAKE_VOLUME_PATH = os.path.join(_FAKE_MNT_POINT,
_FAKE_VOLUME_NAME)
_FAKE_SHARE_OPTS = '-o username=Administrator,password=12345'
@mock.patch.object(smbfs, 'utilsfactory')
@mock.patch.object(smbfs, 'remotefs_brick')
def setUp(self, mock_remotefs, mock_utilsfactory):
super(WindowsSmbFsTestCase, self).setUp()
self.context = context.get_admin_context()
self._FAKE_SMBFS_CONFIG = mock.MagicMock(
smbfs_shares_config=mock.sentinel.share_config_file,
smbfs_default_volume_format='vhdx',
nas_volume_prov_type='thin')
self._smbfs_driver = smbfs.WindowsSmbfsDriver(
configuration=self._FAKE_SMBFS_CONFIG)
self._smbfs_driver._delete = mock.Mock()
self._smbfs_driver._local_volume_dir = mock.Mock(
return_value=self._FAKE_MNT_POINT)
self._smbfs_driver.base = self._FAKE_MNT_BASE
self._diskutils = self._smbfs_driver._diskutils
self._vhdutils = self._smbfs_driver._vhdutils
self.volume = self._simple_volume()
self.snapshot = self._simple_snapshot(volume=self.volume)
self._context = context.get_admin_context()
self.updated_at = timeutils.utcnow()
def _simple_volume(self, **kwargs):
updates = {'id': self._FAKE_VOLUME_ID,
'size': self._FAKE_VOLUME_SIZE,
'provider_location': self._FAKE_SHARE}
updates.update(kwargs)
ctxt = context.get_admin_context()
volume = test_utils.create_volume(ctxt, **updates)
return volume
def _simple_snapshot(self, **kwargs):
volume = kwargs.pop('volume', None) or self._simple_volume()
ctxt = context.get_admin_context()
updates = {'id': self._FAKE_SNAPSHOT_ID,
'volume_id': volume.id}
updates.update(kwargs)
snapshot = test_utils.create_snapshot(ctxt, **updates)
return snapshot
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_check_os_platform')
@mock.patch.object(remotefs.RemoteFSSnapDriverDistributed, 'do_setup')
@mock.patch('os.path.exists')
@mock.patch('os.path.isabs')
@mock.patch.object(image_utils, 'check_qemu_img_version')
def _test_setup(self, mock_check_qemu_img_version,
mock_is_abs, mock_exists,
mock_remotefs_do_setup,
mock_check_os_platform,
config, share_config_exists=True):
mock_exists.return_value = share_config_exists
fake_ensure_mounted = mock.MagicMock()
self._smbfs_driver._ensure_shares_mounted = fake_ensure_mounted
self._smbfs_driver._setup_pool_mappings = mock.Mock()
self._smbfs_driver.configuration = config
if not (config.smbfs_shares_config and share_config_exists):
self.assertRaises(smbfs.SmbfsException,
self._smbfs_driver.do_setup,
mock.sentinel.context)
else:
self._smbfs_driver.do_setup(mock.sentinel.context)
mock_check_qemu_img_version.assert_called_once_with(
self._smbfs_driver._MINIMUM_QEMU_IMG_VERSION)
mock_is_abs.assert_called_once_with(self._smbfs_driver.base)
self.assertEqual({}, self._smbfs_driver.shares)
fake_ensure_mounted.assert_called_once_with()
self._smbfs_driver._setup_pool_mappings.assert_called_once_with()
self.assertTrue(self._smbfs_driver._thin_provisioning_support)
mock_check_os_platform.assert_called_once_with()
def test_setup_pools(self):
pool_mappings = {
'//ip/share0': 'pool0',
'//ip/share1': 'pool1',
}
self._smbfs_driver.configuration.smbfs_pool_mappings = pool_mappings
self._smbfs_driver.shares = {
'//ip/share0': None,
'//ip/share1': None,
'//ip/share2': None
}
expected_pool_mappings = pool_mappings.copy()
expected_pool_mappings['//ip/share2'] = 'share2'
self._smbfs_driver._setup_pool_mappings()
self.assertEqual(expected_pool_mappings,
self._smbfs_driver._pool_mappings)
def test_setup_pool_duplicates(self):
self._smbfs_driver.configuration.smbfs_pool_mappings = {
'share0': 'pool0',
'share1': 'pool0'
}
self.assertRaises(smbfs.SmbfsException,
self._smbfs_driver._setup_pool_mappings)
def test_initialize_connection(self):
self._smbfs_driver.get_active_image_from_info = mock.Mock(
return_value=self._FAKE_VOLUME_NAME)
self._smbfs_driver._get_mount_point_base = mock.Mock(
return_value=self._FAKE_MNT_BASE)
self._smbfs_driver.shares = {self._FAKE_SHARE: self._FAKE_SHARE_OPTS}
self._smbfs_driver.get_volume_format = mock.Mock(
return_value=mock.sentinel.format)
fake_data = {'export': self._FAKE_SHARE,
'format': mock.sentinel.format,
'name': self._FAKE_VOLUME_NAME,
'options': self._FAKE_SHARE_OPTS}
expected = {
'driver_volume_type': 'smbfs',
'data': fake_data,
'mount_point_base': self._FAKE_MNT_BASE}
ret_val = self._smbfs_driver.initialize_connection(
self.volume, None)
self.assertEqual(expected, ret_val)
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_get_snapshot_backing_file')
@mock.patch.object(smbfs.WindowsSmbfsDriver, 'get_volume_format')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_get_mount_point_base')
def test_initialize_connection_snapshot(self, mock_get_mount_base,
mock_get_volume_format,
mock_get_snap_by_backing_file):
self._smbfs_driver.shares = {self._FAKE_SHARE: self._FAKE_SHARE_OPTS}
mock_get_snap_by_backing_file.return_value = self._FAKE_VOLUME_NAME
mock_get_volume_format.return_value = 'vhdx'
mock_get_mount_base.return_value = self._FAKE_MNT_BASE
exp_data = {'export': self._FAKE_SHARE,
'format': 'vhdx',
'name': self._FAKE_VOLUME_NAME,
'options': self._FAKE_SHARE_OPTS,
'access_mode': 'ro'}
expected = {
'driver_volume_type': 'smbfs',
'data': exp_data,
'mount_point_base': self._FAKE_MNT_BASE}
ret_val = self._smbfs_driver.initialize_connection_snapshot(
self.snapshot, mock.sentinel.connector)
self.assertEqual(expected, ret_val)
mock_get_snap_by_backing_file.assert_called_once_with(self.snapshot)
mock_get_volume_format.assert_called_once_with(self.snapshot.volume)
mock_get_mount_base.assert_called_once_with()
def test_setup(self):
self._test_setup(config=self._FAKE_SMBFS_CONFIG)
def test_setup_missing_shares_config_option(self):
fake_config = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_config.smbfs_shares_config = None
self._test_setup(config=fake_config,
share_config_exists=False)
def test_setup_missing_shares_config_file(self):
self._test_setup(config=self._FAKE_SMBFS_CONFIG,
share_config_exists=False)
@mock.patch.object(smbfs, 'context')
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_get_pool_name_from_share')
def test_get_total_allocated(self, mock_get_pool_name, mock_ctxt):
fake_pool_name = 'pool0'
fake_host_name = 'fake_host@fake_backend'
fake_vol_sz_sum = 5
mock_db = mock.Mock()
mock_db.volume_data_get_for_host.return_value = [
mock.sentinel.vol_count, fake_vol_sz_sum]
self._smbfs_driver.host = fake_host_name
self._smbfs_driver.db = mock_db
mock_get_pool_name.return_value = fake_pool_name
allocated = self._smbfs_driver._get_total_allocated(
mock.sentinel.share)
self.assertEqual(fake_vol_sz_sum << 30,
allocated)
mock_get_pool_name.assert_called_once_with(mock.sentinel.share)
mock_db.volume_data_get_for_host.assert_called_once_with(
context=mock_ctxt.get_admin_context.return_value,
host='fake_host@fake_backend#pool0')
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_get_local_volume_path_template')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_lookup_local_volume_path')
@mock.patch.object(smbfs.WindowsSmbfsDriver, 'get_volume_format')
def _test_get_volume_path(self, mock_get_volume_format, mock_lookup_volume,
mock_get_path_template, volume_exists=True):
drv = self._smbfs_driver
(mock_get_path_template.return_value,
ext) = os.path.splitext(self._FAKE_VOLUME_PATH)
volume_format = ext.strip('.')
mock_lookup_volume.return_value = (
self._FAKE_VOLUME_PATH if volume_exists else None)
mock_get_volume_format.return_value = volume_format
ret_val = drv.local_path(self.volume)
if volume_exists:
self.assertFalse(mock_get_volume_format.called)
else:
mock_get_volume_format.assert_called_once_with(self.volume)
self.assertEqual(self._FAKE_VOLUME_PATH, ret_val)
def test_get_existing_volume_path(self):
self._test_get_volume_path()
def test_get_new_volume_path(self):
self._test_get_volume_path(volume_exists=False)
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_local_volume_dir')
def test_get_local_volume_path_template(self, mock_get_local_dir):
mock_get_local_dir.return_value = self._FAKE_MNT_POINT
ret_val = self._smbfs_driver._get_local_volume_path_template(
self.volume)
exp_template = os.path.splitext(self._FAKE_VOLUME_PATH)[0]
self.assertEqual(exp_template, ret_val)
@mock.patch('os.path.exists')
def test_lookup_local_volume_path(self, mock_exists):
expected_path = self._FAKE_VOLUME_PATH + '.vhdx'
mock_exists.side_effect = lambda x: x == expected_path
ret_val = self._smbfs_driver._lookup_local_volume_path(
self._FAKE_VOLUME_PATH)
extensions = [
".%s" % ext
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(
[mock.call(path) for path in possible_paths])
self.assertEqual(expected_path, ret_val)
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_get_local_volume_path_template')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_lookup_local_volume_path')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_get_volume_format_spec')
def _test_get_volume_format(self, mock_get_format_spec,
mock_lookup_volume, mock_get_path_template,
qemu_format=False, volume_format='vhdx',
expected_vol_fmt=None,
volume_exists=True):
expected_vol_fmt = expected_vol_fmt or volume_format
vol_path = '%s.%s' % (os.path.splitext(self._FAKE_VOLUME_PATH)[0],
volume_format)
mock_get_path_template.return_value = vol_path
mock_lookup_volume.return_value = (
vol_path if volume_exists else None)
mock_get_format_spec.return_value = volume_format
supported_fmts = self._smbfs_driver._SUPPORTED_IMAGE_FORMATS
if volume_format.lower() not in supported_fmts:
self.assertRaises(smbfs.SmbfsException,
self._smbfs_driver.get_volume_format,
self.volume,
qemu_format)
else:
ret_val = self._smbfs_driver.get_volume_format(self.volume,
qemu_format)
if volume_exists:
self.assertFalse(mock_get_format_spec.called)
else:
mock_get_format_spec.assert_called_once_with(self.volume)
self.assertEqual(expected_vol_fmt, ret_val)
def test_get_volume_format_invalid_extension(self):
self._test_get_volume_format(volume_format='fake')
def test_get_existing_vhdx_volume_format(self):
self._test_get_volume_format()
def test_get_new_vhd_volume_format(self):
fmt = 'vhd'
self._test_get_volume_format(volume_format=fmt,
volume_exists=False,
expected_vol_fmt=fmt)
def test_get_new_vhd_legacy_volume_format(self):
img_fmt = 'vhd'
expected_fmt = 'vpc'
self._test_get_volume_format(volume_format=img_fmt,
volume_exists=False,
qemu_format=True,
expected_vol_fmt=expected_fmt)
@ddt.data([False, False],
[True, True],
[False, True])
@ddt.unpack
def test_get_volume_format_spec(self,
volume_meta_contains_fmt,
volume_type_contains_fmt):
self._smbfs_driver.configuration = copy.copy(self._FAKE_SMBFS_CONFIG)
fake_vol_meta_fmt = 'vhd'
fake_vol_type_fmt = 'vhdx'
volume_metadata = {}
volume_type_extra_specs = {}
if volume_meta_contains_fmt:
volume_metadata['volume_format'] = fake_vol_meta_fmt
elif volume_type_contains_fmt:
volume_type_extra_specs['smbfs:volume_format'] = fake_vol_type_fmt
volume_type = fake_volume.fake_volume_type_obj(self.context)
volume = fake_volume.fake_volume_obj(self.context)
# Optional arguments are not set in _from_db_object,
# so have to set explicitly here
volume.volume_type = volume_type
volume.metadata = volume_metadata
# Same for extra_specs and VolumeType
volume_type.extra_specs = volume_type_extra_specs
resulted_fmt = self._smbfs_driver._get_volume_format_spec(volume)
if volume_meta_contains_fmt:
expected_fmt = fake_vol_meta_fmt
elif volume_type_contains_fmt:
expected_fmt = fake_vol_type_fmt
else:
expected_fmt = self._FAKE_SMBFS_CONFIG.smbfs_default_volume_format
self.assertEqual(expected_fmt, resulted_fmt)
@mock.patch.object(remotefs.RemoteFSSnapDriverDistributed,
'create_volume')
def test_create_volume_base(self, mock_create_volume):
self._smbfs_driver.create_volume(self.volume)
mock_create_volume.assert_called_once_with(self.volume)
@mock.patch('os.path.exists')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_get_vhd_type')
def _test_create_volume(self, mock_get_vhd_type, mock_exists,
volume_exists=False, volume_format='vhdx'):
mock_exists.return_value = volume_exists
self._smbfs_driver.create_vhd = mock.MagicMock()
fake_create = self._smbfs_driver._vhdutils.create_vhd
self._smbfs_driver.get_volume_format = mock.Mock(
return_value=volume_format)
if volume_exists or volume_format not in ('vhd', 'vhdx'):
self.assertRaises(exception.InvalidVolume,
self._smbfs_driver._do_create_volume,
self.volume)
else:
fake_vol_path = self._FAKE_VOLUME_PATH
self._smbfs_driver._do_create_volume(self.volume)
fake_create.assert_called_once_with(
fake_vol_path, mock_get_vhd_type.return_value,
max_internal_size=self.volume.size << 30,
guid=self.volume.id)
def test_create_volume(self):
self._test_create_volume()
def test_create_existing_volume(self):
self._test_create_volume(True)
def test_create_volume_invalid_volume(self):
self._test_create_volume(volume_format="qcow")
def test_delete_volume(self):
drv = self._smbfs_driver
fake_vol_info = self._FAKE_VOLUME_PATH + '.info'
drv._ensure_share_mounted = mock.MagicMock()
fake_ensure_mounted = drv._ensure_share_mounted
drv._local_volume_dir = mock.Mock(
return_value=self._FAKE_MNT_POINT)
drv.get_active_image_from_info = mock.Mock(
return_value=self._FAKE_VOLUME_NAME)
drv._delete = mock.Mock()
drv._local_path_volume_info = mock.Mock(
return_value=fake_vol_info)
with mock.patch('os.path.exists', lambda x: True):
drv.delete_volume(self.volume)
fake_ensure_mounted.assert_called_once_with(self._FAKE_SHARE)
drv._delete.assert_any_call(
self._FAKE_VOLUME_PATH)
drv._delete.assert_any_call(fake_vol_info)
def test_ensure_mounted(self):
self._smbfs_driver.shares = {self._FAKE_SHARE: self._FAKE_SHARE_OPTS}
self._smbfs_driver._ensure_share_mounted(self._FAKE_SHARE)
self._smbfs_driver._remotefsclient.mount.assert_called_once_with(
self._FAKE_SHARE, self._FAKE_SHARE_OPTS)
def test_get_capacity_info(self):
self._diskutils.get_disk_capacity.return_value = (
self._FAKE_TOTAL_SIZE, self._FAKE_TOTAL_AVAILABLE)
self._smbfs_driver._get_mount_point_for_share = mock.Mock(
return_value=mock.sentinel.mnt_point)
self._smbfs_driver._get_total_allocated = mock.Mock(
return_value=self._FAKE_TOTAL_ALLOCATED)
ret_val = self._smbfs_driver._get_capacity_info(self._FAKE_SHARE)
expected_ret_val = [int(x) for x in [self._FAKE_TOTAL_SIZE,
self._FAKE_TOTAL_AVAILABLE,
self._FAKE_TOTAL_ALLOCATED]]
self.assertEqual(expected_ret_val, ret_val)
self._smbfs_driver._get_mount_point_for_share.assert_called_once_with(
self._FAKE_SHARE)
self._diskutils.get_disk_capacity.assert_called_once_with(
mock.sentinel.mnt_point)
self._smbfs_driver._get_total_allocated.assert_called_once_with(
self._FAKE_SHARE)
def _test_get_img_info(self, backing_file=None):
self._smbfs_driver._vhdutils.get_vhd_parent_path.return_value = (
backing_file)
image_info = self._smbfs_driver._qemu_img_info(self._FAKE_VOLUME_PATH)
self.assertEqual(self._FAKE_VOLUME_NAME,
image_info.image)
backing_file_name = backing_file and os.path.basename(backing_file)
self.assertEqual(backing_file_name, image_info.backing_file)
def test_get_img_info_without_backing_file(self):
self._test_get_img_info()
def test_get_snapshot_info(self):
self._test_get_img_info(self._FAKE_VOLUME_PATH)
@ddt.data('attached', 'detached')
def test_create_snapshot(self, attach_status):
self.snapshot.volume.attach_status = attach_status
self.snapshot.volume.save()
self._smbfs_driver._vhdutils.create_differencing_vhd = (
mock.Mock())
self._smbfs_driver._local_volume_dir = mock.Mock(
return_value=self._FAKE_MNT_POINT)
fake_create_diff = (
self._smbfs_driver._vhdutils.create_differencing_vhd)
self._smbfs_driver._do_create_snapshot(
self.snapshot,
os.path.basename(self._FAKE_VOLUME_PATH),
self._FAKE_SNAPSHOT_PATH)
if attach_status != 'attached':
fake_create_diff.assert_called_once_with(self._FAKE_SNAPSHOT_PATH,
self._FAKE_VOLUME_PATH)
else:
fake_create_diff.assert_not_called()
self.assertEqual(os.path.basename(self._FAKE_VOLUME_PATH),
self.snapshot.metadata['backing_file'])
# Ensure that the changes have been saved.
self.assertFalse(bool(self.snapshot.obj_what_changed()))
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_check_extend_volume_support')
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_local_path_active_image')
def test_extend_volume(self, mock_get_active_img,
mock_check_ext_support):
volume = fake_volume.fake_volume_obj(self.context)
new_size = volume.size + 1
self._smbfs_driver.extend_volume(volume, new_size)
mock_check_ext_support.assert_called_once_with(volume, new_size)
mock_get_active_img.assert_called_once_with(volume)
self._vhdutils.resize_vhd.assert_called_once_with(
mock_get_active_img.return_value,
new_size * units.Gi,
is_file_max_size=False)
@ddt.data({'snapshots_exist': True},
{'vol_fmt': smbfs.WindowsSmbfsDriver._DISK_FORMAT_VHD,
'snapshots_exist': True,
'expected_exc': exception.InvalidVolume})
@ddt.unpack
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'get_volume_format')
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_snapshots_exist')
def test_check_extend_support(self, mock_snapshots_exist,
mock_get_volume_format,
vol_fmt=None, snapshots_exist=False,
share_eligible=True,
expected_exc=None):
vol_fmt = vol_fmt or self._smbfs_driver._DISK_FORMAT_VHDX
volume = fake_volume.fake_volume_obj(
self.context, provider_location='fake_provider_location')
new_size = volume.size + 1
mock_snapshots_exist.return_value = snapshots_exist
mock_get_volume_format.return_value = vol_fmt
if expected_exc:
self.assertRaises(expected_exc,
self._smbfs_driver._check_extend_volume_support,
volume, new_size)
else:
self._smbfs_driver._check_extend_volume_support(volume, new_size)
mock_get_volume_format.assert_called_once_with(volume)
mock_snapshots_exist.assert_called_once_with(volume)
@ddt.data({},
{'delete_latest': True},
{'attach_status': 'detached'},
{'snap_info_contains_snap_id': False})
@ddt.unpack
@mock.patch.object(remotefs.RemoteFSSnapDriverDistributed,
'_delete_snapshot')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_local_volume_dir')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_local_path_volume_info')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_write_info_file')
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_read_info_file')
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_nova_assisted_vol_snap_delete')
@mock.patch.object(smbfs.WindowsSmbfsDriver,
'_get_snapshot_by_backing_file')
def test_delete_snapshot(self, mock_get_snap_by_backing_file,
mock_nova_assisted_snap_del,
mock_read_info_file, mock_write_info_file,
mock_local_path_volume_info,
mock_get_local_dir,
mock_remotefs_snap_delete,
attach_status='attached',
snap_info_contains_snap_id=True,
delete_latest=False):
self.snapshot.volume.attach_status = attach_status
self.snapshot.metadata['backing_file'] = os.path.basename(
self._FAKE_VOLUME_PATH)
higher_snapshot = self._simple_snapshot(id=None,
volume=self.volume)
fake_snap_file = 'snap_file'
fake_snap_parent_path = os.path.join(self._FAKE_MNT_POINT,
'snap_file_parent')
active_img = 'active_img' if not delete_latest else fake_snap_file
snap_info = dict(active=active_img)
if snap_info_contains_snap_id:
snap_info[self.snapshot.id] = fake_snap_file
mock_get_snap_by_backing_file.return_value = (
higher_snapshot if not delete_latest else None)
mock_info_path = mock_local_path_volume_info.return_value
mock_read_info_file.return_value = snap_info
mock_get_local_dir.return_value = self._FAKE_MNT_POINT
self._vhdutils.get_vhd_parent_path.return_value = (
fake_snap_parent_path)
expected_delete_info = {'file_to_merge': fake_snap_file,
'volume_id': self.snapshot.volume.id}
self._smbfs_driver._delete_snapshot(self.snapshot)
if attach_status != 'attached':
mock_remotefs_snap_delete.assert_called_once_with(self.snapshot)
elif snap_info_contains_snap_id:
mock_local_path_volume_info.assert_called_once_with(
self.snapshot.volume)
mock_read_info_file.assert_called_once_with(
mock_info_path, empty_if_missing=True)
mock_nova_assisted_snap_del.assert_called_once_with(
self.snapshot._context, self.snapshot, expected_delete_info)
exp_merged_img_path = os.path.join(self._FAKE_MNT_POINT,
fake_snap_file)
self._smbfs_driver._delete.assert_called_once_with(
exp_merged_img_path)
if delete_latest:
self._vhdutils.get_vhd_parent_path.assert_called_once_with(
exp_merged_img_path)
exp_active = os.path.basename(fake_snap_parent_path)
else:
exp_active = active_img
self.assertEqual(exp_active, snap_info['active'])
self.assertNotIn(snap_info, self.snapshot.id)
mock_write_info_file.assert_called_once_with(mock_info_path,
snap_info)
if attach_status != 'attached' or not snap_info_contains_snap_id:
mock_nova_assisted_snap_del.assert_not_called()
mock_write_info_file.assert_not_called()
if not delete_latest and snap_info_contains_snap_id:
self.assertEqual(os.path.basename(self._FAKE_VOLUME_PATH),
higher_snapshot.metadata['backing_file'])
self.assertFalse(bool(higher_snapshot.obj_what_changed()))
@ddt.data(True, False)
def test_get_snapshot_by_backing_file(self, metadata_set):
backing_file = 'fake_backing_file'
if metadata_set:
self.snapshot.metadata['backing_file'] = backing_file
self.snapshot.save()
for idx in range(2):
# We're adding a few other snapshots.
self._simple_snapshot(id=None,
volume=self.volume)
snapshot = self._smbfs_driver._get_snapshot_by_backing_file(
self.volume, backing_file)
if metadata_set:
self.assertEqual(self.snapshot.id, snapshot.id)
else:
self.assertIsNone(snapshot)
@ddt.data(True, False)
@mock.patch.object(remotefs.RemoteFSSnapDriverDistributed,
'_get_snapshot_backing_file')
def test_get_snapshot_backing_file_md_set(self, md_set,
remotefs_get_backing_file):
backing_file = 'fake_backing_file'
if md_set:
self.snapshot.metadata['backing_file'] = backing_file
ret_val = self._smbfs_driver._get_snapshot_backing_file(
self.snapshot)
# If the metadata is not set, we expect the super class method to
# be used, which is supposed to query the image.
if md_set:
self.assertEqual(backing_file, ret_val)
else:
self.assertEqual(remotefs_get_backing_file.return_value,
ret_val)
remotefs_get_backing_file.assert_called_once_with(
self.snapshot)
def test_create_volume_from_unavailable_snapshot(self):
self.snapshot.status = fields.SnapshotStatus.ERROR
self.assertRaises(
exception.InvalidSnapshot,
self._smbfs_driver.create_volume_from_snapshot,
self.volume, self.snapshot)
@ddt.data(True, False)
def test_copy_volume_to_image(self, has_parent=False):
drv = self._smbfs_driver
volume = test_utils.create_volume(
self._context, volume_type_id=fake.VOLUME_TYPE_ID,
updated_at=self.updated_at)
extra_specs = {
'image_service:store_id': 'fake-store'
}
test_utils.create_volume_type(self._context.elevated(),
id=fake.VOLUME_TYPE_ID, name="test_type",
extra_specs=extra_specs)
fake_image_meta = {'id': 'fake-image-id'}
fake_img_format = self._smbfs_driver._DISK_FORMAT_VHDX
if has_parent:
fake_volume_path = self._FAKE_SNAPSHOT_PATH
fake_parent_path = self._FAKE_VOLUME_PATH
else:
fake_volume_path = self._FAKE_VOLUME_PATH
fake_parent_path = None
fake_active_image = os.path.basename(fake_volume_path)
drv.get_active_image_from_info = mock.Mock(
return_value=fake_active_image)
drv._local_volume_dir = mock.Mock(
return_value=self._FAKE_MNT_POINT)
drv.get_volume_format = mock.Mock(
return_value=fake_img_format)
drv._vhdutils.get_vhd_parent_path.return_value = (
fake_parent_path)
with mock.patch.object(image_utils, 'upload_volume') as (
fake_upload_volume):
drv.copy_volume_to_image(
mock.sentinel.context, volume,
mock.sentinel.image_service, fake_image_meta)
if has_parent:
fake_temp_image_name = '%s.temp_image.%s.%s' % (
volume.id,
fake_image_meta['id'],
fake_img_format)
fake_temp_image_path = os.path.join(
self._FAKE_MNT_POINT,
fake_temp_image_name)
fake_active_image_path = os.path.join(
self._FAKE_MNT_POINT,
fake_active_image)
upload_path = fake_temp_image_path
drv._vhdutils.convert_vhd.assert_called_once_with(
fake_active_image_path,
fake_temp_image_path)
drv._delete.assert_called_once_with(
fake_temp_image_path)
else:
upload_path = fake_volume_path
fake_upload_volume.assert_called_once_with(
mock.sentinel.context, mock.sentinel.image_service,
fake_image_meta, upload_path, volume_fd=None,
volume_format=fake_img_format,
store_id='fake-store', base_image_ref=None, compress=True,
run_as_root=True)
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_get_vhd_type')
def test_copy_image_to_volume(self, mock_get_vhd_type):
drv = self._smbfs_driver
drv.get_volume_format = mock.Mock(
return_value=mock.sentinel.volume_format)
drv.local_path = mock.Mock(
return_value=self._FAKE_VOLUME_PATH)
drv.configuration = mock.MagicMock()
drv.configuration.volume_dd_blocksize = mock.sentinel.block_size
with mock.patch.object(image_utils,
'fetch_to_volume_format') as fake_fetch:
drv.copy_image_to_volume(
mock.sentinel.context, self.volume,
mock.sentinel.image_service,
mock.sentinel.image_id)
fake_fetch.assert_called_once_with(
mock.sentinel.context,
mock.sentinel.image_service,
mock.sentinel.image_id,
self._FAKE_VOLUME_PATH, mock.sentinel.volume_format,
mock.sentinel.block_size,
mock_get_vhd_type.return_value,
disable_sparse=False)
drv._vhdutils.resize_vhd.assert_called_once_with(
self._FAKE_VOLUME_PATH,
self.volume.size * units.Gi,
is_file_max_size=False)
drv._vhdutils.set_vhd_guid.assert_called_once_with(
self._FAKE_VOLUME_PATH,
self.volume.id)
@mock.patch.object(smbfs.WindowsSmbfsDriver, '_get_vhd_type')
def test_copy_volume_from_snapshot(self, mock_get_vhd_type):
drv = self._smbfs_driver
drv._get_snapshot_backing_file = mock.Mock(
return_value=self._FAKE_VOLUME_NAME)
drv._local_volume_dir = mock.Mock(
return_value=self._FAKE_MNT_POINT)
drv.local_path = mock.Mock(
return_value=mock.sentinel.new_volume_path)
drv._copy_volume_from_snapshot(self.snapshot,
self.volume, self.volume.size)
drv._get_snapshot_backing_file.assert_called_once_with(
self.snapshot)
drv._delete.assert_called_once_with(mock.sentinel.new_volume_path)
drv._vhdutils.convert_vhd.assert_called_once_with(
self._FAKE_VOLUME_PATH,
mock.sentinel.new_volume_path,
vhd_type=mock_get_vhd_type.return_value)
drv._vhdutils.set_vhd_guid.assert_called_once_with(
mock.sentinel.new_volume_path,
self.volume.id)
drv._vhdutils.resize_vhd.assert_called_once_with(
mock.sentinel.new_volume_path,
self.volume.size * units.Gi,
is_file_max_size=False)
def test_copy_encrypted_volume_from_snapshot(self):
# We expect an exception to be raised if an encryption
# key is provided since we don't support encryted volumes
# for the time being.
self.assertRaises(exception.NotSupportedOperation,
self._smbfs_driver._copy_volume_from_snapshot,
self.snapshot, self.volume,
self.volume.size,
mock.sentinel.src_key,
mock.sentinel.dest_key)
def test_rebase_img(self):
drv = self._smbfs_driver
drv._rebase_img(
self._FAKE_SNAPSHOT_PATH,
self._FAKE_VOLUME_NAME, 'vhdx')
drv._vhdutils.reconnect_parent_vhd.assert_called_once_with(
self._FAKE_SNAPSHOT_PATH, self._FAKE_VOLUME_PATH)
def test_copy_volume_image(self):
self._smbfs_driver._copy_volume_image(mock.sentinel.src,
mock.sentinel.dest)
self._smbfs_driver._pathutils.copy.assert_called_once_with(
mock.sentinel.src, mock.sentinel.dest)
def test_get_pool_name_from_share(self):
self._smbfs_driver._pool_mappings = {
mock.sentinel.share: mock.sentinel.pool}
pool = self._smbfs_driver._get_pool_name_from_share(
mock.sentinel.share)
self.assertEqual(mock.sentinel.pool, pool)
def test_get_share_from_pool_name(self):
self._smbfs_driver._pool_mappings = {
mock.sentinel.share: mock.sentinel.pool}
share = self._smbfs_driver._get_share_from_pool_name(
mock.sentinel.pool)
self.assertEqual(mock.sentinel.share, share)
def test_get_pool_name_from_share_exception(self):
self._smbfs_driver._pool_mappings = {}
self.assertRaises(smbfs.SmbfsException,
self._smbfs_driver._get_share_from_pool_name,
mock.sentinel.pool)
def test_get_vhd_type(self):
drv = self._smbfs_driver
mock_type = drv._get_vhd_type(qemu_subformat=True)
self.assertEqual(mock_type, 'dynamic')
mock_type = drv._get_vhd_type(qemu_subformat=False)
self.assertEqual(mock_type, 3)
self._smbfs_driver.configuration.nas_volume_prov_type = (
'thick')
mock_type = drv._get_vhd_type(qemu_subformat=True)
self.assertEqual(mock_type, 'fixed')
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)
@mock.patch.object(remotefs.RemoteFSManageableVolumesMixin,
'manage_existing')
def test_manage_existing(self, remotefs_manage):
model_update = dict(provider_location=self._FAKE_SHARE)
remotefs_manage.return_value = model_update
self._smbfs_driver.local_path = mock.Mock(
return_value=mock.sentinel.vol_path)
# Let's make sure that the provider location gets set.
# It's needed by self.local_path.
self.volume.provider_location = None
ret_val = self._smbfs_driver.manage_existing(
self.volume, mock.sentinel.ref)
self.assertEqual(model_update, ret_val)
self.assertEqual(self._FAKE_SHARE, self.volume.provider_location)
self._vhdutils.set_vhd_guid.assert_called_once_with(
mock.sentinel.vol_path,
self.volume.id)
self._smbfs_driver.local_path.assert_called_once_with(self.volume)
remotefs_manage.assert_called_once_with(self.volume, mock.sentinel.ref)
-6
View File
@@ -460,12 +460,6 @@ def temporary_chown(path: str,
:params owner_uid: UID of temporary owner (defaults to current user)
"""
if os.name == 'nt':
LOG.debug("Skipping chown for %s as this operation is "
"not available on Windows.", path)
yield
return
if owner_uid is None:
owner_uid = os.getuid()
+1 -3
View File
@@ -844,9 +844,7 @@ class RemoteFSSnapDriverBase(RemoteFSDriver):
msg = _("'active' must be present when writing snap_info.")
raise exception.RemoteFSException(msg)
if not (os.path.exists(info_path) or os.name == 'nt'):
# We're not managing file permissions on Windows.
# Plus, 'truncate' is not available.
if not os.path.exists(info_path):
self._execute('truncate', "-s0", info_path,
run_as_root=self._execute_as_root)
self._set_rw_permissions(info_path)
@@ -1,17 +0,0 @@
# Copyright 2014 Cloudbase Solutions Srl
#
# 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.
VHD_TYPE_FIXED = 2
VHD_TYPE_DYNAMIC = 3
VHD_TYPE_DIFFERENCING = 4
-360
View File
@@ -1,360 +0,0 @@
# Copyright 2012 Pedro Navarro Perez
# All Rights Reserved.
#
# 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.
"""
Volume driver for Windows Server 2012
This driver requires ISCSI target role installed
"""
import contextlib
import os
from os_win import utilsfactory
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import fileutils
from oslo_utils import units
from oslo_utils import uuidutils
from cinder.common import constants
from cinder import exception
from cinder.image import image_utils
from cinder import interface
from cinder.volume import configuration
from cinder.volume import driver
from cinder.volume import volume_utils
LOG = logging.getLogger(__name__)
windows_opts = [
cfg.StrOpt('windows_iscsi_lun_path',
default=r'C:\iSCSIVirtualDisks',
help='Path to store VHD backed volumes'),
]
CONF = cfg.CONF
CONF.register_opts(windows_opts, group=configuration.SHARED_CONF_GROUP)
@interface.volumedriver
class WindowsISCSIDriver(driver.ISCSIDriver):
"""Executes volume driver commands on Windows Storage server."""
VERSION = '1.0.0'
# ThirdPartySystems wiki page
CI_WIKI_NAME = "Microsoft_iSCSI_CI"
SUPPORTED = False
def __init__(self, *args, **kwargs):
super(WindowsISCSIDriver, self).__init__(*args, **kwargs)
self.configuration = kwargs.get('configuration', None)
if self.configuration:
self.configuration.append_config_values(windows_opts)
self._vhdutils = utilsfactory.get_vhdutils()
self._tgt_utils = utilsfactory.get_iscsi_target_utils()
self._hostutils = utilsfactory.get_hostutils()
@staticmethod
def get_driver_options():
return windows_opts
def do_setup(self, context):
"""Setup the Windows Volume driver.
Called one time by the manager after the driver is loaded.
Validate the flags we care about
"""
fileutils.ensure_tree(self.configuration.windows_iscsi_lun_path)
fileutils.ensure_tree(CONF.image_conversion_dir)
def check_for_setup_error(self):
"""Check that the driver is working and can communicate."""
self._get_portals()
def _get_portals(self):
available_portals = set(self._tgt_utils.get_portal_locations(
available_only=True,
fail_if_none_found=True))
LOG.debug("Available iSCSI portals: %s", available_portals)
iscsi_port = self.configuration.target_port
iscsi_ips = ([self.configuration.target_ip_address] +
self.configuration.target_secondary_ip_addresses)
requested_portals = {':'.join([iscsi_ip, str(iscsi_port)])
for iscsi_ip in iscsi_ips}
unavailable_portals = requested_portals - available_portals
if unavailable_portals:
LOG.warning("The following iSCSI portals were requested but "
"are not available: %s.", unavailable_portals)
selected_portals = requested_portals & available_portals
if not selected_portals:
err_msg = "None of the configured iSCSI portals are available."
raise exception.VolumeDriverException(err_msg)
return list(selected_portals)
def _get_host_information(self, volume, multipath=False):
"""Getting the portal and port information."""
target_name = self._get_target_name(volume)
available_portals = self._get_portals()
properties = self._tgt_utils.get_target_information(target_name)
# Note(lpetrut): the WT_Host CHAPSecret field cannot be accessed
# for security reasons.
auth = volume.provider_auth
if auth:
(auth_method, auth_username, auth_secret) = auth.split()
properties['auth_method'] = auth_method
properties['auth_username'] = auth_username
properties['auth_password'] = auth_secret
properties['target_portal'] = available_portals[0]
properties['target_discovered'] = False
properties['target_lun'] = 0
properties['volume_id'] = volume.id
if multipath:
properties['target_portals'] = available_portals
properties['target_iqns'] = [properties['target_iqn']
for portal in available_portals]
properties['target_luns'] = [properties['target_lun']
for portal in available_portals]
return properties
def initialize_connection(self, volume, connector):
"""Driver entry point to attach a volume to an instance."""
initiator_name = connector['initiator']
target_name = volume.provider_location
self._tgt_utils.associate_initiator_with_iscsi_target(initiator_name,
target_name)
properties = self._get_host_information(volume,
connector.get('multipath'))
return {
'driver_volume_type': 'iscsi',
'data': properties,
}
def terminate_connection(self, volume, connector, **kwargs):
"""Driver entry point to unattach a volume from an instance.
Unmask the LUN on the storage system so the given initiator can no
longer access it.
"""
initiator_name = connector['initiator']
target_name = volume.provider_location
self._tgt_utils.deassociate_initiator(initiator_name, target_name)
def create_volume(self, volume):
"""Driver entry point for creating a new volume."""
vhd_path = self.local_path(volume)
vol_name = volume.name
vol_size_mb = volume.size * 1024
self._tgt_utils.create_wt_disk(vhd_path, vol_name,
size_mb=vol_size_mb)
def local_path(self, volume, disk_format=None):
base_vhd_folder = self.configuration.windows_iscsi_lun_path
if not disk_format:
disk_format = self._tgt_utils.get_supported_disk_format()
disk_fname = "%s.%s" % (volume.name, disk_format)
return os.path.join(base_vhd_folder, disk_fname)
def delete_volume(self, volume):
"""Driver entry point for destroying existing volumes."""
vol_name = volume.name
vhd_path = self.local_path(volume)
self._tgt_utils.remove_wt_disk(vol_name)
fileutils.delete_if_exists(vhd_path)
def create_snapshot(self, snapshot):
"""Driver entry point for creating a snapshot."""
# Getting WT_Snapshot class
vol_name = snapshot.volume_name
snapshot_name = snapshot.name
self._tgt_utils.create_snapshot(vol_name, snapshot_name)
def create_volume_from_snapshot(self, volume, snapshot):
"""Driver entry point for exporting snapshots as volumes."""
snapshot_name = snapshot.name
vol_name = volume.name
vhd_path = self.local_path(volume)
self._tgt_utils.export_snapshot(snapshot_name, vhd_path)
self._tgt_utils.import_wt_disk(vhd_path, vol_name)
def delete_snapshot(self, snapshot):
"""Driver entry point for deleting a snapshot."""
snapshot_name = snapshot.name
self._tgt_utils.delete_snapshot(snapshot_name)
def ensure_export(self, context, volume):
# iSCSI targets exported by WinTarget persist after host reboot.
pass
def _get_target_name(self, volume):
return "%s%s" % (self.configuration.target_prefix,
volume.name)
def create_export(self, context, volume, connector):
"""Driver entry point to get the export info for a new volume."""
target_name = self._get_target_name(volume)
updates = {}
if not self._tgt_utils.iscsi_target_exists(target_name):
self._tgt_utils.create_iscsi_target(target_name)
updates['provider_location'] = target_name
if self.configuration.use_chap_auth:
chap_username = (self.configuration.chap_username or
volume_utils.generate_username())
chap_password = (self.configuration.chap_password or
volume_utils.generate_password())
self._tgt_utils.set_chap_credentials(target_name,
chap_username,
chap_password)
updates['provider_auth'] = ' '.join(('CHAP',
chap_username,
chap_password))
# This operation is idempotent
self._tgt_utils.add_disk_to_target(volume.name, target_name)
return updates
def remove_export(self, context, volume):
"""Driver entry point to remove an export for a volume."""
target_name = self._get_target_name(volume)
self._tgt_utils.delete_iscsi_target(target_name)
def copy_image_to_volume(self, context, volume, image_service, image_id,
disable_sparse=False):
"""Fetch the image from image_service and create a volume using it."""
# Convert to VHD and file back to VHD
vhd_type = self._tgt_utils.get_supported_vhd_type()
with image_utils.temporary_file(suffix='.vhd') as tmp:
volume_path = self.local_path(volume)
image_utils.fetch_to_vhd(context, image_service, image_id, tmp,
self.configuration.volume_dd_blocksize,
disable_sparse=disable_sparse)
# The vhd must be disabled and deleted before being replaced with
# the desired image.
self._tgt_utils.change_wt_disk_status(volume.name,
enabled=False)
os.unlink(volume_path)
self._vhdutils.convert_vhd(tmp, volume_path,
vhd_type)
self._vhdutils.resize_vhd(volume_path,
volume.size << 30,
is_file_max_size=False)
self._tgt_utils.change_wt_disk_status(volume.name,
enabled=True)
@contextlib.contextmanager
def _temporary_snapshot(self, volume_name):
try:
snap_uuid = uuidutils.generate_uuid()
snapshot_name = '%s-tmp-snapshot-%s' % (volume_name, snap_uuid)
self._tgt_utils.create_snapshot(volume_name, snapshot_name)
yield snapshot_name
finally:
self._tgt_utils.delete_snapshot(snapshot_name)
def copy_volume_to_image(self, context, volume, image_service, image_meta):
"""Copy the volume to the specified image."""
disk_format = self._tgt_utils.get_supported_disk_format()
temp_vhd_path = os.path.join(CONF.image_conversion_dir,
str(image_meta['id']) + '.' + disk_format)
try:
with self._temporary_snapshot(volume.name) as tmp_snap_name:
# qemu-img cannot access VSS snapshots, for which reason it
# must be exported first.
self._tgt_utils.export_snapshot(tmp_snap_name, temp_vhd_path)
volume_utils.upload_volume(
context, image_service, image_meta, temp_vhd_path, volume,
'vhd')
finally:
fileutils.delete_if_exists(temp_vhd_path)
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
src_vol_name = src_vref.name
vol_name = volume.name
vol_size = volume.size
new_vhd_path = self.local_path(volume)
with self._temporary_snapshot(src_vol_name) as tmp_snap_name:
self._tgt_utils.export_snapshot(tmp_snap_name, new_vhd_path)
self._vhdutils.resize_vhd(new_vhd_path, vol_size << 30,
is_file_max_size=False)
self._tgt_utils.import_wt_disk(new_vhd_path, vol_name)
def _get_capacity_info(self):
drive = os.path.splitdrive(
self.configuration.windows_iscsi_lun_path)[0]
(size, free_space) = self._hostutils.get_volume_info(drive)
total_gb = size / units.Gi
free_gb = free_space / units.Gi
return (total_gb, free_gb)
def _update_volume_stats(self):
"""Retrieve stats info for Windows device."""
LOG.debug("Updating volume stats")
total_gb, free_gb = self._get_capacity_info()
data = {}
backend_name = self.configuration.safe_get('volume_backend_name')
data["volume_backend_name"] = backend_name or self.__class__.__name__
data["vendor_name"] = 'Microsoft'
data["driver_version"] = self.VERSION
data["storage_protocol"] = constants.ISCSI
data['total_capacity_gb'] = total_gb
data['free_capacity_gb'] = free_gb
data['reserved_percentage'] = self.configuration.reserved_percentage
data['QoS_support'] = False
self._stats = data
def extend_volume(self, volume, new_size):
"""Extend an Existing Volume."""
old_size = volume.size
LOG.debug("Extend volume from %(old_size)s GB to %(new_size)s GB.",
{'old_size': old_size, 'new_size': new_size})
additional_size_mb = (new_size - old_size) * 1024
self._tgt_utils.extend_wt_disk(volume.name, additional_size_mb)
def backup_use_temp_snapshot(self):
return False
-697
View File
@@ -1,697 +0,0 @@
# Copyright (c) 2014 Cloudbase Solutions SRL
# All Rights Reserved.
#
# 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 os
import re
import sys
from os_brick.remotefs import windows_remotefs as remotefs_brick
from os_win import constants as os_win_const
from os_win import utilsfactory
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 context
from cinder import coordination
from cinder import exception
from cinder.i18n import _
from cinder.image import image_utils
from cinder import interface
from cinder import objects
from cinder import utils
from cinder.volume import configuration
from cinder.volume.drivers import remotefs as remotefs_drv
from cinder.volume import volume_utils
VERSION = '1.1.0'
LOG = logging.getLogger(__name__)
volume_opts = [
cfg.StrOpt('smbfs_shares_config',
default=r'C:\OpenStack\smbfs_shares.txt',
help='File with the list of available smbfs shares.'),
cfg.StrOpt('smbfs_default_volume_format',
default='vhd',
choices=['vhd', 'vhdx'],
help=('Default format that will be used when creating volumes '
'if no volume format is specified.')),
cfg.StrOpt('smbfs_mount_point_base',
default=r'C:\OpenStack\_mnt',
help=('Base dir containing mount points for smbfs shares.')),
cfg.DictOpt('smbfs_pool_mappings',
default={},
help=('Mappings between share locations and pool names. '
'If not specified, the share names will be used as '
'pool names. Example: '
'//addr/share:pool_name,//addr/share2:pool_name2')),
]
CONF = cfg.CONF
CONF.register_opts(volume_opts, group=configuration.SHARED_CONF_GROUP)
class SmbfsException(exception.RemoteFSException):
message = _("Unknown SMBFS exception.")
@interface.volumedriver
class WindowsSmbfsDriver(remotefs_drv.RevertToSnapshotMixin,
remotefs_drv.RemoteFSPoolMixin,
remotefs_drv.RemoteFSManageableVolumesMixin,
remotefs_drv.RemoteFSSnapDriverDistributed):
VERSION = VERSION
driver_volume_type = 'smbfs'
driver_prefix = 'smbfs'
volume_backend_name = 'Generic_SMBFS'
SHARE_FORMAT_REGEX = r'//.+/.+'
VERSION = VERSION
_DISK_FORMAT_VHD = 'vhd'
_DISK_FORMAT_VHD_LEGACY = 'vpc'
_DISK_FORMAT_VHDX = 'vhdx'
# ThirdPartySystems wiki page
CI_WIKI_NAME = "Cloudbase_Cinder_SMB3_CI"
SUPPORTED = False
_MINIMUM_QEMU_IMG_VERSION = '1.6'
_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(
r'.*\.(?:%s)$' % '|'.join(_VALID_IMAGE_EXTENSIONS),
re.IGNORECASE)
_always_use_temp_snap_when_cloning = False
_thin_provisioning_support = True
_vhd_type_mapping = {'thin': os_win_const.VHD_TYPE_DYNAMIC,
'thick': os_win_const.VHD_TYPE_FIXED}
_vhd_qemu_subformat_mapping = {'thin': 'dynamic',
'thick': 'fixed'}
def __init__(self, *args, **kwargs):
self._remotefsclient = None
super(WindowsSmbfsDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(volume_opts)
self.base = getattr(self.configuration,
'smbfs_mount_point_base')
self._remotefsclient = remotefs_brick.WindowsRemoteFsClient(
'cifs', root_helper=None, smbfs_mount_point_base=self.base,
local_path_for_loopback=True)
self._vhdutils = utilsfactory.get_vhdutils()
self._pathutils = utilsfactory.get_pathutils()
self._smbutils = utilsfactory.get_smbutils()
self._diskutils = utilsfactory.get_diskutils()
thin_enabled = (
self.configuration.nas_volume_prov_type == 'thin')
self._thin_provisioning_support = thin_enabled
self._thick_provisioning_support = not thin_enabled
@staticmethod
def get_driver_options():
return volume_opts
def do_setup(self, context):
self._check_os_platform()
super(WindowsSmbfsDriver, self).do_setup(context)
image_utils.check_qemu_img_version(self._MINIMUM_QEMU_IMG_VERSION)
config = self.configuration.smbfs_shares_config
if not config:
msg = (_("SMBFS config file not set (smbfs_shares_config)."))
LOG.error(msg)
raise SmbfsException(msg)
if not os.path.exists(config):
msg = (_("SMBFS config file at %(config)s doesn't exist.") %
{'config': config})
LOG.error(msg)
raise SmbfsException(msg)
if not os.path.isabs(self.base):
msg = _("Invalid mount point base: %s") % self.base
LOG.error(msg)
raise SmbfsException(msg)
self.shares = {} # address : options
self._ensure_shares_mounted()
self._setup_pool_mappings()
def _setup_pool_mappings(self):
self._pool_mappings = self.configuration.smbfs_pool_mappings
pools = list(self._pool_mappings.values())
duplicate_pools = set([pool for pool in pools
if pools.count(pool) > 1])
if duplicate_pools:
msg = _("Found multiple mappings for pools %(pools)s. "
"Requested pool mappings: %(pool_mappings)s")
raise SmbfsException(
msg % dict(pools=duplicate_pools,
pool_mappings=self._pool_mappings))
shares_missing_mappings = (
set(self.shares).difference(set(self._pool_mappings)))
for share in shares_missing_mappings:
msg = ("No pool name was requested for share %(share)s "
"Using the share name instead.")
LOG.warning(msg, dict(share=share))
self._pool_mappings[share] = self._get_share_name(share)
@coordination.synchronized('{self.driver_prefix}-{volume.id}')
def initialize_connection(self, volume, connector):
"""Allow connection to connector and return connection info.
:param volume: volume reference
:param connector: connector reference
"""
# Find active image
active_file = self.get_active_image_from_info(volume)
fmt = self.get_volume_format(volume)
data = {'export': volume.provider_location,
'format': fmt,
'name': active_file}
if volume.provider_location in self.shares:
data['options'] = self.shares[volume.provider_location]
return {
'driver_volume_type': self.driver_volume_type,
'data': data,
'mount_point_base': self._get_mount_point_base()
}
@coordination.synchronized('{self.driver_prefix}-{snapshot.volume.id}')
def initialize_connection_snapshot(self, snapshot, connector):
backing_file = self._get_snapshot_backing_file(snapshot)
volume = snapshot.volume
fmt = self.get_volume_format(volume)
data = {'export': volume.provider_location,
'format': fmt,
'name': backing_file,
'access_mode': 'ro'}
if volume.provider_location in self.shares:
data['options'] = self.shares[volume.provider_location]
return {
'driver_volume_type': self.driver_volume_type,
'data': data,
'mount_point_base': self._get_mount_point_base()
}
def _check_os_platform(self):
if sys.platform != 'win32':
_msg = _("This system platform (%s) is not supported. This "
"driver supports only Win32 platforms.") % sys.platform
raise SmbfsException(_msg)
def _get_total_allocated(self, smbfs_share):
pool_name = self._get_pool_name_from_share(smbfs_share)
host = "#".join([self.host, pool_name])
vol_sz_sum = self.db.volume_data_get_for_host(
context=context.get_admin_context(),
host=host)[1]
return float(vol_sz_sum * units.Gi)
def local_path(self, volume):
"""Get volume path (mounted locally fs path) for given volume.
:param volume: volume reference
"""
volume_path_template = self._get_local_volume_path_template(volume)
volume_path = self._lookup_local_volume_path(volume_path_template)
if volume_path:
return volume_path
# The image does not exist, so retrieve the volume format
# in order to build the path.
fmt = self.get_volume_format(volume)
volume_path = volume_path_template + '.' + fmt
return volume_path
def _get_local_volume_path_template(self, volume):
local_dir = self._local_volume_dir(volume)
local_path_template = os.path.join(local_dir, volume.name)
return local_path_template
def _lookup_local_volume_path(self, volume_path_template):
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):
return volume_path
def _get_new_snap_path(self, snapshot):
vol_path = self.local_path(snapshot.volume)
snap_path, ext = os.path.splitext(vol_path)
snap_path += '.' + snapshot.id + ext
return snap_path
def get_volume_format(self, volume, qemu_format=False):
volume_path_template = self._get_local_volume_path_template(volume)
volume_path = self._lookup_local_volume_path(volume_path_template)
if volume_path:
ext = os.path.splitext(volume_path)[1].strip('.').lower()
if ext in self._VALID_IMAGE_EXTENSIONS:
volume_format = ext
else:
# Hyper-V relies on file extensions so we're enforcing them.
raise SmbfsException(
_("Invalid image file extension: %s") % ext)
else:
volume_format = (
self._get_volume_format_spec(volume) or
self.configuration.smbfs_default_volume_format)
if qemu_format and volume_format == self._DISK_FORMAT_VHD:
volume_format = self._DISK_FORMAT_VHD_LEGACY
elif volume_format == self._DISK_FORMAT_VHD_LEGACY:
volume_format = self._DISK_FORMAT_VHD
return volume_format
def _get_volume_format_spec(self, volume):
vol_type = volume.volume_type
extra_specs = {}
if vol_type and vol_type.extra_specs:
extra_specs = vol_type.extra_specs
extra_specs.update(volume.metadata or {})
return (extra_specs.get('volume_format') or
extra_specs.get('smbfs:volume_format') or
self.configuration.smbfs_default_volume_format)
@coordination.synchronized('{self.driver_prefix}-{volume.id}')
def create_volume(self, volume):
return super(WindowsSmbfsDriver, self).create_volume(volume)
def _do_create_volume(self, volume):
volume_path = self.local_path(volume)
volume_format = self.get_volume_format(volume)
volume_size_bytes = volume.size * units.Gi
if os.path.exists(volume_path):
err_msg = _('File already exists at: %s') % volume_path
raise exception.InvalidVolume(err_msg)
if volume_format not in self._SUPPORTED_IMAGE_FORMATS:
err_msg = _("Unsupported volume format: %s ") % volume_format
raise exception.InvalidVolume(err_msg)
vhd_type = self._get_vhd_type()
self._vhdutils.create_vhd(volume_path, vhd_type,
max_internal_size=volume_size_bytes,
guid=volume.id)
def _ensure_share_mounted(self, smbfs_share):
mnt_flags = None
if self.shares.get(smbfs_share) is not None:
mnt_flags = self.shares[smbfs_share]
self._remotefsclient.mount(smbfs_share, mnt_flags)
@coordination.synchronized('{self.driver_prefix}-{volume.id}')
def delete_volume(self, volume):
"""Deletes a logical volume."""
if not volume.provider_location:
LOG.warning('Volume %s does not have provider_location '
'specified, skipping.', volume.name)
return
self._ensure_share_mounted(volume.provider_location)
volume_dir = self._local_volume_dir(volume)
mounted_path = os.path.join(volume_dir,
self.get_active_image_from_info(volume))
if os.path.exists(mounted_path):
self._delete(mounted_path)
else:
LOG.debug("Skipping deletion of volume %s as it does not exist.",
mounted_path)
info_path = self._local_path_volume_info(volume)
self._delete(info_path)
def _delete(self, path):
fileutils.delete_if_exists(path)
def _get_capacity_info(self, smbfs_share):
"""Calculate available space on the SMBFS share.
:param smbfs_share: example //172.18.194.100/var/smbfs
"""
mount_point = self._get_mount_point_for_share(smbfs_share)
total_size, total_available = self._diskutils.get_disk_capacity(
mount_point)
total_allocated = self._get_total_allocated(smbfs_share)
return_value = [total_size, total_available, total_allocated]
LOG.info('Smb share %(share)s Total size %(size)s '
'Total allocated %(allocated)s',
{'share': smbfs_share, 'size': total_size,
'allocated': total_allocated})
return [float(x) for x in return_value]
def _img_commit(self, snapshot_path):
self._vhdutils.merge_vhd(snapshot_path)
def _rebase_img(self, image, backing_file, volume_format):
# Relative path names are not supported in this case.
image_dir = os.path.dirname(image)
backing_file_path = os.path.join(image_dir, backing_file)
self._vhdutils.reconnect_parent_vhd(image, backing_file_path)
def _qemu_img_info(self, path, volume_name=None):
# This code expects to deal only with relative filenames.
# As this method is needed by the upper class and qemu-img does
# not fully support vhdx images, for the moment we'll use Win32 API
# for retrieving image information.
parent_path = self._vhdutils.get_vhd_parent_path(path)
file_format = os.path.splitext(path)[1][1:].lower()
if parent_path:
backing_file_name = os.path.split(parent_path)[1].lower()
else:
backing_file_name = None
class ImageInfo(object):
def __init__(self, image, backing_file):
self.image = image
self.backing_file = backing_file
self.file_format = file_format
return ImageInfo(os.path.basename(path),
backing_file_name)
def _do_create_snapshot(self, snapshot, backing_file, new_snap_path):
if self._is_volume_attached(snapshot.volume):
LOG.debug("Snapshot is in-use. Performing Nova "
"assisted creation.")
else:
backing_file_full_path = os.path.join(
self._local_volume_dir(snapshot.volume),
backing_file)
self._vhdutils.create_differencing_vhd(new_snap_path,
backing_file_full_path)
# We're setting the backing file information in the DB as we may not
# be able to query the image while it's in use due to file locks.
#
# When dealing with temporary snapshots created by the driver, we
# may not receive an actual snapshot VO. We currently need this check
# in order to avoid breaking the volume clone operation.
#
# TODO(lpetrut): remove this check once we'll start using db entries
# for such temporary snapshots, most probably when we'll add support
# for cloning in-use volumes.
if isinstance(snapshot, objects.Snapshot):
snapshot.metadata['backing_file'] = backing_file
snapshot.save()
else:
LOG.debug("Received a '%s' object, skipping setting the backing "
"file in the DB.", type(snapshot))
def _extend_volume(self, volume, size_gb):
self._check_extend_volume_support(volume, size_gb)
volume_path = self._local_path_active_image(volume)
LOG.info('Resizing file %(volume_path)s to %(size_gb)sGB.',
dict(volume_path=volume_path, size_gb=size_gb))
self._vhdutils.resize_vhd(volume_path, size_gb * units.Gi,
is_file_max_size=False)
def _delete_snapshot(self, snapshot):
# NOTE(lpetrut): We're slightly diverging from the super class
# workflow. The reason is that we cannot query in-use vhd/x images,
# nor can we add or remove images from a vhd/x chain in this case.
info_path = self._local_path_volume_info(snapshot.volume)
snap_info = self._read_info_file(info_path, empty_if_missing=True)
if snapshot.id not in snap_info:
LOG.info('Snapshot record for %s is not present, allowing '
'snapshot_delete to proceed.', snapshot.id)
return
file_to_merge = snap_info[snapshot.id]
deleting_latest_snap = utils.paths_normcase_equal(snap_info['active'],
file_to_merge)
if not self._is_volume_attached(snapshot.volume):
super(WindowsSmbfsDriver, self)._delete_snapshot(snapshot)
else:
delete_info = {'file_to_merge': file_to_merge,
'volume_id': snapshot.volume.id}
self._nova_assisted_vol_snap_delete(
snapshot._context, snapshot, delete_info)
# At this point, the image file should no longer be in use, so we
# may safely query it so that we can update the 'active' image
# reference, if needed.
merged_img_path = os.path.join(
self._local_volume_dir(snapshot.volume),
file_to_merge)
if deleting_latest_snap:
new_active_file_path = self._vhdutils.get_vhd_parent_path(
merged_img_path).lower()
snap_info['active'] = os.path.basename(new_active_file_path)
self._delete(merged_img_path)
# TODO(lpetrut): drop snapshot info file usage.
del snap_info[snapshot.id]
self._write_info_file(info_path, snap_info)
if not isinstance(snapshot, objects.Snapshot):
LOG.debug("Received a '%s' object, skipping setting the backing "
"file in the DB.", type(snapshot))
elif not deleting_latest_snap:
backing_file = snapshot['metadata'].get('backing_file')
higher_snapshot = self._get_snapshot_by_backing_file(
snapshot.volume, file_to_merge)
# The snapshot objects should have a backing file set, unless
# created before an upgrade. If the snapshot we're deleting
# does not have a backing file set yet there is a newer one that
# does, we're clearing it out so that it won't provide wrong info.
if higher_snapshot:
LOG.debug("Updating backing file reference (%(backing_file)s) "
"for higher snapshot: %(higher_snapshot_id)s.",
dict(backing_file=snapshot.metadata['backing_file'],
higher_snapshot_id=higher_snapshot.id))
higher_snapshot.metadata['backing_file'] = (
snapshot.metadata['backing_file'])
higher_snapshot.save()
if not (higher_snapshot and backing_file):
LOG.info(
"The deleted snapshot is not latest one, yet we could not "
"find snapshot backing file information in the DB. This "
"may happen after an upgrade. Certain operations against "
"this volume may be unavailable while it's in-use.")
def _get_snapshot_by_backing_file(self, volume, backing_file):
all_snapshots = objects.SnapshotList.get_all_for_volume(
context.get_admin_context(), volume.id)
for snapshot in all_snapshots:
snap_backing_file = snapshot.metadata.get('backing_file')
if utils.paths_normcase_equal(snap_backing_file or '',
backing_file):
return snapshot
def _get_snapshot_backing_file(self, snapshot):
backing_file = snapshot.metadata.get('backing_file')
if not backing_file:
LOG.info("Could not find the snapshot backing file in the DB. "
"This may happen after an upgrade. Attempting to "
"query the image as a fallback. This may fail if "
"the image is in-use.")
backing_file = super(
WindowsSmbfsDriver, self)._get_snapshot_backing_file(snapshot)
return backing_file
def _check_extend_volume_support(self, volume, size_gb):
snapshots_exist = self._snapshots_exist(volume)
fmt = self.get_volume_format(volume)
if snapshots_exist and fmt == self._DISK_FORMAT_VHD:
msg = _('Extending volumes backed by VHD images is not supported '
'when snapshots exist. Please use VHDX images.')
raise exception.InvalidVolume(msg)
@coordination.synchronized('{self.driver_prefix}-{volume.id}')
def copy_volume_to_image(self, context, volume, image_service, image_meta):
"""Copy the volume to the specified image."""
# If snapshots exist, flatten to a temporary image, and upload it
active_file = self.get_active_image_from_info(volume)
active_file_path = os.path.join(self._local_volume_dir(volume),
active_file)
backing_file = self._vhdutils.get_vhd_parent_path(active_file_path)
root_file_fmt = self.get_volume_format(volume)
temp_path = None
try:
if backing_file:
temp_file_name = '%s.temp_image.%s.%s' % (
volume.id,
image_meta['id'],
root_file_fmt)
temp_path = os.path.join(self._local_volume_dir(volume),
temp_file_name)
self._vhdutils.convert_vhd(active_file_path, temp_path)
upload_path = temp_path
else:
upload_path = active_file_path
volume_utils.upload_volume(context,
image_service,
image_meta,
upload_path,
volume,
root_file_fmt)
finally:
if temp_path:
self._delete(temp_path)
def copy_image_to_volume(self, context, volume, image_service, image_id,
disable_sparse=False):
"""Fetch the image from image_service and write it to the volume."""
volume_path = self.local_path(volume)
volume_format = self.get_volume_format(volume, qemu_format=True)
volume_subformat = self._get_vhd_type(qemu_subformat=True)
self._delete(volume_path)
image_utils.fetch_to_volume_format(
context, image_service, image_id,
volume_path, volume_format,
self.configuration.volume_dd_blocksize,
volume_subformat,
disable_sparse=disable_sparse)
volume_path = self.local_path(volume)
self._vhdutils.set_vhd_guid(volume_path, volume.id)
self._vhdutils.resize_vhd(volume_path,
volume.size * units.Gi,
is_file_max_size=False)
def _copy_volume_from_snapshot(self, snapshot, volume, volume_size,
src_encryption_key_id=None,
new_encryption_key_id=None):
"""Copy data from snapshot to destination volume."""
if new_encryption_key_id:
msg = _("Encryption key %s was requested. Volume "
"encryption is not currently supported.")
raise exception.NotSupportedOperation(
message=msg % new_encryption_key_id)
LOG.debug("snapshot: %(snap)s, volume: %(vol)s, "
"volume_size: %(size)s",
{'snap': snapshot.id,
'vol': volume.id,
'size': snapshot.volume_size})
vol_dir = self._local_volume_dir(snapshot.volume)
# Find the file which backs this file, which represents the point
# when this snapshot was created.
backing_file = self._get_snapshot_backing_file(snapshot)
snapshot_path = os.path.join(vol_dir, backing_file)
volume_path = self.local_path(volume)
vhd_type = self._get_vhd_type()
self._delete(volume_path)
self._vhdutils.convert_vhd(snapshot_path,
volume_path,
vhd_type=vhd_type)
self._vhdutils.set_vhd_guid(volume_path, volume.id)
self._vhdutils.resize_vhd(volume_path, volume_size * units.Gi,
is_file_max_size=False)
def _copy_volume_image(self, src_path, dest_path):
self._pathutils.copy(src_path, dest_path)
def _get_share_name(self, share):
return share.replace('/', '\\').lstrip('\\').split('\\', 1)[1]
def _get_pool_name_from_share(self, share):
return self._pool_mappings[share]
def _get_share_from_pool_name(self, pool_name):
mappings = {pool: share
for share, pool in self._pool_mappings.items()}
share = mappings.get(pool_name)
if not share:
msg = _("Could not find any share for pool %(pool_name)s. "
"Pool mappings: %(pool_mappings)s.")
raise SmbfsException(
msg % dict(pool_name=pool_name,
pool_mappings=self._pool_mappings))
return share
def _get_vhd_type(self, qemu_subformat=False):
prov_type = self.configuration.nas_volume_prov_type
if qemu_subformat:
vhd_type = self._vhd_qemu_subformat_mapping[prov_type]
else:
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 manage_existing(self, volume, existing_ref):
model_update = super(WindowsSmbfsDriver, self).manage_existing(
volume, existing_ref)
volume.provider_location = model_update['provider_location']
volume_path = self.local_path(volume)
self._vhdutils.set_vhd_guid(volume_path, volume.id)
return model_update
def _set_rw_permissions(self, path):
# The SMBFS driver does not manage file permissions. We chose
# to let this up to the deployer.
pass
def backup_use_temp_snapshot(self):
return True
@@ -39,9 +39,6 @@ public or private clouds.
cinderlib) is aligned with the Ceph server version. Mixing server
and client versions is *unsupported* and may lead to anomalous behavior.
The minimum requirements for using Ceph with Hyper-V are Ceph Pacific and
Windows Server 2016.
RADOS
~~~~~
@@ -1,72 +0,0 @@
.. _windows_iscsi_volume_driver:
===========================
Windows iSCSI volume driver
===========================
Windows Server offers an integrated iSCSI Target service that can be used with
OpenStack Block Storage in your stack.
Being entirely a software solution, consider it in particular for mid-sized
networks where the costs of a SAN might be excessive.
The Windows iSCSI Block Storage driver works with OpenStack Compute on any
hypervisor.
This driver creates volumes backed by fixed-type VHD images on Windows Server
2012 and dynamic-type VHDX on Windows Server 2012 R2 and onwards, stored
locally on a user-specified path. The system uses those images as iSCSI disks
and exports them through iSCSI targets. Each volume has its own iSCSI target.
The ``cinder-volume`` service as well as the required Python components will
be installed directly onto the Windows node.
Prerequisites
~~~~~~~~~~~~~
The Windows iSCSI volume driver depends on the ``wintarget`` Windows service.
This will require the ``iSCSI Target Server`` Windows feature to be installed.
.. note::
The Cinder MSI will automatically enable this feature, if available (some
minimal Windows versions do not provide it).
You may check the availability of this feature by running the following:
.. code-block:: powershell
Get-WindowsFeature FS-iSCSITarget-Server
.. end
.. end
The Windows Server installation requires at least 16 GB of disk space. The
volumes hosted by this node will need extra space.
Configuring cinder-volume
~~~~~~~~~~~~~~~~~~~~~~~~~
Below is a configuration sample for using the Windows iSCSI Driver. Append
those options to your already existing ``cinder.conf`` file, described at
:ref:`cinder_storage_install_windows`.
.. code-block:: ini
[DEFAULT]
enabled_backends = winiscsi
[winiscsi]
volume_driver = cinder.volume.drivers.windows.iscsi.WindowsISCSIDriver
windows_iscsi_lun_path = C:\iSCSIVirtualDisks
volume_backend_name = winiscsi
# The following config options are optional
#
# use_chap_auth = true
# target_port = 3260
# target_ip_addres = <IP_USED_FOR_ISCSI_TRAFFIC>
# iscsi_secondary_ip_addresses = <SECONDARY_ISCSI_IPS>
# reserved_percentage = 5
.. end
The ``windows_iscsi_lun_path`` config option specifies the directory in
which VHD backed volumes will be stored.
@@ -1,249 +0,0 @@
.. _windows_smb_volume_driver:
=========================
Windows SMB volume driver
=========================
Description
~~~~~~~~~~~
The Windows SMB volume driver leverages pre-existing SMB shares, used to store
volumes as virtual disk images.
The main reasons to use the Windows SMB driver are:
* ease of management and use
* great integration with other Microsoft technologies (e.g. Hyper-V Failover
Cluster)
* suitable for a various range of deployment types and sizes
The ``cinder-volume`` service as well as the required Python components will
be installed directly onto designated Windows nodes (preferably the ones
exposing the shares).
Common deployment scenarios
---------------------------
The SMB driver is designed to support a variety of scenarios, such as:
* Scale-Out File Servers (``SoFS``), providing highly available SMB shares.
* standalone Windows or Samba shares
* any other SMB 3.0 capable device
By using SoFS shares, the virtual disk images are stored on Cluster Shared
Volumes (``CSV``).
A common practice involves deploying CSVs on top of SAN backed LUNs
(exposed to all the nodes of the cluster through iSCSI or Fibre Channel). In
absence of a SAN, Storage Spaces/Storage Spaces Direct (``S2D``) may be used
for the underlying storage.
.. note::
S2D is commonly used in hyper-converged deployments.
.. end
Features
--------
``VHD`` and ``VHDX`` are the currently supported image formats and may be
consumed by Hyper-V and KVM compute nodes. By default, dynamic (thinly
provisioned) images will be used, unless configured otherwise.
The driver accepts one or more shares that will be reported to the Cinder
scheduler as storage pools. This can provide means of tiering, allowing
specific shares (pools) to be requested through volume types.
.. code-block:: console
openstack volume type set $volume_type --property pool_name=$pool_name
.. end
Frontend QoS specs may be associated with the volume types and enforced on the
consumer side (e.g. Hyper-V).
.. code-block:: console
openstack volume qos create $rule_name --property consumer=front-end --property total_bytes_sec=20971520
openstack volume qos associate $rule_name $volume_type_id
openstack volume create $volume_name --type $volume_type_id --size $size
.. end
The ``Cinder Backup Service`` can be run on Windows. This driver stores
the volumes using vhdx images stored on SMB shares which can be attached
in order to retrieve the volume data and send it to the backup service.
Prerequisites:
* All physical disks must be in byte mode
* rb+ must be used when writing backups to disk
Clustering support
------------------
Active-Active Cinder clustering is currently experimental and should not be
used in production. This implies having multiple Cinder Volume services
handling the same share simultaneously.
On the other hand, Active-Passive clustering can easily be achieved,
configuring the Cinder Volume service as clustered using Microsoft Failover
Cluster.
By using SoFS, you can provide high availability of the shares used by Cinder.
This can be used in conjunction with the Nova Hyper-V cluster driver, which
allows clustering virtual machines. This ensures that when a compute node is
compromised, the virtual machines are transparently migrated to a healthy
node, preserving volume connectivity.
.. note::
The Windows SMB driver is the only Cinder driver that may be used along
with the Nova Hyper-V cluster driver. The reason is that during an
unexpected failover, the volumes need to be available on the destination
compute node side.
.. _windows_smb_volume_driver_prerequisites:
Prerequisites
~~~~~~~~~~~~~
Before setting up the SMB driver, you will need to create and configure one or
more SMB shares that will be used for storing virtual disk images.
.. note::
The driver does not manage share permissions. You will have to make sure
that Cinder as well as share consumers (e.g. Nova, Hyper-V) have access.
Note that Hyper-V VMs are run using a built-in user group:
``NT VIRTUAL MACHINE\Virtual Machines``.
.. end
The easiest way to provide share access is by using Active Directory accounts.
You may grant share access to the users running OpenStack services, as well as
the compute nodes (and optionally storage nodes), using per computer account
access rules. One of the main advantages is that by doing so, you don't need
to pass share credentials to Cinder (and implicitly volume consumers).
By granting access to a computer account, you're basically granting access to
the LocalSystem account of that node, and thus to the VMs running on that
host.
.. note::
By default, OpenStack services deployed using the MSIs are run as
LocalSystem.
Once you've granted share access to a specific account, don't forget to also
configure file system level permissions on the directory exported by the
share.
Configuring cinder-volume
~~~~~~~~~~~~~~~~~~~~~~~~~
Below is a configuration sample for using the Windows SMB Driver. Append
those options to your already existing ``cinder.conf`` file, described at
:ref:`cinder_storage_install_windows`.
.. code-block:: ini
[DEFAULT]
enabled_backends = winsmb
[winsmb]
volume_backend_name = myWindowsSMBBackend
volume_driver = cinder.volume.drivers.windows.smbfs.WindowsSmbfsDriver
smbfs_mount_point_base = C:\OpenStack\mnt\
smbfs_shares_config = C:\Program Files\Cloudbase Solutions\OpenStack\etc\cinder\smbfs_shares_list
# The following config options are optional
#
# image_volume_cache_enabled = true
# image_volume_cache_max_size_gb = 100
# image_volume_cache_max_count = 10
#
# nas_volume_prov_type = thin
# smbfs_default_volume_format = vhdx
# max_over_subscription_ratio = 1.5
# reserved_percentage = 5
# smbfs_pool_mappings = //addr/share:pool_name,//addr/share2:pool_name2
.. end
The ``smbfs_mount_point_base`` config option allows you to specify where
the shares will be *mounted*. This directory will contain symlinks pointing
to the shares used by Cinder. Each symlink name will be a hash of the actual
share path.
Configuring the list of available shares
----------------------------------------
In addition to ``cinder.conf``, you will need to have another config file,
providing a list of shares that will be used by Cinder for storing disk
images. In the above sample, this file is referenced by the
``smbfs_shares_config`` option.
The share list config file must contain one share per line, optionally
including mount options. You may also add comments, using a '#' at the
beginning of the line.
Bellow is a sample of the share list config file:
.. code-block:: ini
# Cinder Volume shares
//sofs-cluster/share
//10.0.0.10/volumes -o username=user,password=mypassword
.. end
Keep in mind that Linux hosts can also consume those volumes. For this
reason, the mount options resemble the ones used by mount.cifs (in fact,
those will actually be passed to mount.cifs by the Nova Linux nodes).
In case of Windows nodes, only the share location, username and password
will be used when mounting the shares. The share address must use slashes
instead of backslashes (as opposed to what Windows admins may expect) because
of the above mentioned reason.
Depending on the configured share access rules, you may skip including
share credentials in the config file, as described in the
:ref:`windows_smb_volume_driver_prerequisites` section.
Configuring Nova credentials
----------------------------
The SMB volume driver relies on the ``nova assisted volume snapshots`` feature
when snapshotting in-use volumes, as do other similar drivers using shared
filesystems.
By default, the Nova policy requires admin rights for this operation. You may
provide Cinder specific credentials to be used when requesting Nova assisted
volume snapshots, as shown bellow:
.. code-block:: ini
[nova]
region_name=RegionOne
auth_strategy=keystone
auth_type=password
auth_url=http://keystone_host/identity
project_name=service
username=nova
password=password
project_domain_name=Default
user_domain_name=Default
.. end
Configuring storage pools
-------------------------
Each share is reported to the Cinder scheduler as a storage pool.
By default, the share name will be the name of the pool. If needed, you may
provide pool name mappings, specifying a custom pool name for each share,
as shown bellow:
.. code-block:: ini
smbfs_pool_mappings = //addr/share:pool0
.. end
In the above sample, the ``//addr/share`` share will be reported as ``pool0``.
@@ -1,176 +0,0 @@
.. _cinder_storage_install_windows:
Install and configure a storage node
====================================
Prerequisites
~~~~~~~~~~~~~
The following Windows versions are officially supported by Cinder:
* ``Windows Server 2012``
* ``Windows Server 2012 R2``
* ``Windows Server 2016``
The OpenStack Cinder Volume MSI installer is the recommended deployment tool
for Cinder on Windows. You can find it at
https://cloudbase.it/openstack-windows-storage/#download.
It installs an independent Python environment, in order to avoid conflicts
with existing applications. It can dynamically generate a ``cinder.conf`` file
based on the parameters you provide.
The OpenStack Cinder Volume MSI installer can be deployed in a fully automated
way using Puppet, Chef, SaltStack, Ansible, Juju, DSC, Windows Group Policies
or any other automated configuration framework.
Configure NTP
-------------
Network time services must be configured to ensure proper operation
of the OpenStack nodes. To set network time on your Windows host you
must run the following commands:
.. code-block:: bat
net stop w32time
w32tm /config /manualpeerlist:pool.ntp.org,0x8 /syncfromflags:MANUAL
net start w32time
Keep in mind that the node will have to be time synchronized with
the other nodes of your OpenStack environment, so it is important to use
the same NTP server.
.. note::
In case of an Active Directory environment, you may do this only for the
AD Domain Controller.
.. end
Install and configure components
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The MSI may be run in the following modes:
Graphical mode
--------------
The installer will walk you through the commonly used cinder options,
automatically generating a config file based on your input.
You may run the following in order to run the installer in graphical mode,
also specifying a log file. Please use the installer full path.
.. code-block:: powershell
msiexec /i CinderVolumeSetup.msi /l*v msi_log.txt
.. end
Unattended mode
---------------
The installer will deploy Cinder, taking care of required Windows services and
features. A minimal sample config file will be generated and need to be
updated accordingly.
Run the following in order to install Cinder in unattended mode, enabling the
iSCSI and SMB volume drivers.
.. code-block:: powershell
msiexec /i CinderVolumeSetup.msi /qn /l*v msi_log.txt `
ADDLOCAL="iscsiDriver,smbDriver"
.. end
By default, Cinder will be installed at
``%ProgramFiles%\Cloudbase Solutions\OpenStack``. You may choose a different
install directory by using the ``INSTALLDIR`` argument, as following:
.. code-block:: powershell
msiexec /i CinderVolumeSetup.msi /qn /l*v msi_log.txt `
ADDLOCAL="iscsiDriver,smbDriver" `
INSTALLDIR="C:\cinder"
.. end
The installer will generate a Windows service, called ``cinder-volume``.
.. note::
Previous MSI releases may use a separate service per volume backend (e.g.
cinder-volume-smb). You may double check the cinder services along with
their executable paths by running the following:
.. code-block:: powershell
get-service cinder-volume*
sc.exe qc cinder-volume-smb
.. end
Note that ``sc`` is also an alias for ``Set-Content``. To use the service
control utility, you have to explicitly call ``sc.exe``.
.. end
Configuring Cinder
------------------
If you've run the installer in graphical mode, you may skip this part as the
MSI already took care of generating the configuration files.
The Cinder Volume Windows service configured by the MSI expects the cinder
config file to reside at::
%INSTALLDIR%\etc\cinder.conf
You may use the following config sample, updating fields appropriately.
.. code-block:: ini
[DEFAULT]
my_ip = MANAGEMENT_INTERFACE_IP_ADDRESS
auth_strategy = keystone
transport_url = rabbit://RABBIT_USER:RABBIT_PASS@controller:5672
glance_api_servers = http://controller/image
sql_connection = mysql+pymysql://cinder:CINDER_DBPASS@controller/cinder
image_conversion_dir = C:\OpenStack\ImageConversionDir\
lock_path = C:\OpenStack\Lock\
log_dir = C:\OpenStack\Log\
log_file = cinder-volume.log
[coordination]
backend_url = file:///C:/OpenStack/Lock/
[key_manager]
api_class = cinder.keymgr.conf_key_mgr.ConfKeyManager
.. end
.. note::
The above sample doesn't configure any Cinder Volume driver. To do
so, follow the configuration guide for the driver of choice, appending
driver specific config options.
.. end
Currently supported drivers on Windows:
* :ref:`windows_smb_volume_driver`
* :ref:`windows_iscsi_volume_driver`
Finalize installation
~~~~~~~~~~~~~~~~~~~~~
#. Restart the Cinder Volume service:
.. code-block:: powershell
Restart-Service cinder-volume
.. end
#. Ensure that the Cinder Volume service is running:
.. code-block:: powershell
Get-Service cinder-volume
.. end
-15
View File
@@ -1,15 +0,0 @@
=====================================
Cinder Installation Guide for Windows
=====================================
This section describes how to install and configure storage nodes
for the Block Storage service.
For the moment, Cinder Volume is the only Cinder service supported on
Windows.
.. toctree::
:maxdepth: 2
cinder-storage-install-windows.rst
cinder-verify.rst
-1
View File
@@ -48,5 +48,4 @@ The following links describe how to install the Cinder Block Storage Service:
index-obs
index-rdo
index-ubuntu
index-windows
-26
View File
@@ -231,12 +231,6 @@ title=Virtuozzo Storage Driver (remotefs)
[driver.vmware]
title=VMware Storage Driver (vmdk)
[driver.win_iscsi]
title=Windows iSCSI Driver
[driver.win_smb]
title=Windows SMB Driver
[driver.yadro]
title=Yadro Tatlin Unified Driver (iSCSI, FC)
@@ -325,8 +319,6 @@ driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
driver.vmware=complete
driver.win_iscsi=missing
driver.win_smb=missing
driver.yadro=complete
driver.zadara=complete
@@ -407,8 +399,6 @@ driver.vrtsaccess=complete
driver.vrtscnfs=complete
driver.vzstorage=complete
driver.vmware=complete
driver.win_iscsi=complete
driver.win_smb=complete
driver.yadro=complete
driver.zadara=complete
@@ -492,8 +482,6 @@ driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
driver.vmware=missing
driver.win_iscsi=missing
driver.win_smb=missing
driver.yadro=complete
driver.zadara=missing
@@ -576,8 +564,6 @@ driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
driver.vmware=missing
driver.win_iscsi=missing
driver.win_smb=missing
driver.yadro=missing
driver.zadara=missing
@@ -661,8 +647,6 @@ driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
driver.vmware=missing
driver.win_iscsi=missing
driver.win_smb=missing
driver.yadro=missing
driver.zadara=missing
@@ -745,8 +729,6 @@ driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
driver.vmware=missing
driver.win_iscsi=missing
driver.win_smb=complete
driver.yadro=complete
driver.zadara=missing
@@ -830,8 +812,6 @@ driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
driver.vmware=missing
driver.win_iscsi=missing
driver.win_smb=missing
driver.yadro=missing
driver.zadara=missing
@@ -915,8 +895,6 @@ driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
driver.vmware=missing
driver.win_iscsi=missing
driver.win_smb=missing
driver.yadro=complete
driver.zadara=complete
@@ -997,8 +975,6 @@ driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
driver.vmware=complete
driver.win_iscsi=missing
driver.win_smb=missing
driver.yadro=complete
driver.zadara=missing
@@ -1083,7 +1059,5 @@ driver.vrtsaccess=missing
driver.vrtscnfs=missing
driver.vzstorage=missing
driver.vmware=missing
driver.win_iscsi=missing
driver.win_smb=missing
driver.yadro=complete
driver.zadara=missing
@@ -0,0 +1,11 @@
---
upgrade:
- |
The following volume drivers have been removed.
- ``Windows iSCSI driver``
- ``Windows SMB driver``
- |
Support for running cinder services in Windows operating systems has been
removed.
-1
View File
@@ -50,7 +50,6 @@ WebOb>=1.8.6 # MIT
oslo.i18n>=5.1.0 # Apache-2.0
oslo.vmware>=3.10.0 # Apache-2.0
os-brick>=6.10.0 # Apache-2.0
os-win>=5.5.0 # Apache-2.0
tooz>=2.8.0 # Apache-2.0
google-api-python-client>=1.11.0 # Apache-2.0
castellan>=3.7.0 # Apache-2.0
-1
View File
@@ -5,7 +5,6 @@
# Install bounded pep8/pyflakes first, then let flake8 install
hacking>=7.0.0,<7.1.0 # Apache-2.0
flake8-import-order<0.19.0 # LGPLv3
flake8-logging-format>=0.6.0 # Apache-2.0
stestr>=3.2.1 # Apache-2.0
coverage>=5.5 # Apache-2.0