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:
@@ -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'':
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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', }
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -48,5 +48,4 @@ The following links describe how to install the Cinder Block Storage Service:
|
||||
index-obs
|
||||
index-rdo
|
||||
index-ubuntu
|
||||
index-windows
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user