Merge "rbd Windows support"
This commit is contained in:
commit
9b0a91039a
|
@ -43,6 +43,7 @@ windows_connector_list = [
|
|||
'os_brick.initiator.windows.base.BaseWindowsConnector',
|
||||
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
|
||||
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
|
||||
'os_brick.initiator.windows.rbd.WindowsRBDConnector',
|
||||
'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector'
|
||||
]
|
||||
|
||||
|
@ -159,6 +160,8 @@ _connector_mapping_windows = {
|
|||
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
|
||||
initiator.FIBRE_CHANNEL:
|
||||
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
|
||||
initiator.RBD:
|
||||
'os_brick.initiator.windows.rbd.WindowsRBDConnector',
|
||||
initiator.SMBFS:
|
||||
'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# Copyright 2020 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.
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import netutils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RBDConnectorMixin(object):
|
||||
"""Mixin covering cross platform RBD connector functionality"""
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_mon_hosts(hosts):
|
||||
def _sanitize_host(host):
|
||||
if netutils.is_valid_ipv6(host):
|
||||
host = '[%s]' % host
|
||||
return host
|
||||
return list(map(_sanitize_host, hosts))
|
||||
|
||||
@classmethod
|
||||
def _get_rbd_args(cls, connection_properties, conf=None):
|
||||
user = connection_properties.get('auth_username')
|
||||
monitor_ips = connection_properties.get('hosts')
|
||||
monitor_ports = connection_properties.get('ports')
|
||||
|
||||
args = []
|
||||
if user:
|
||||
args = ['--id', user]
|
||||
if monitor_ips and monitor_ports:
|
||||
monitors = ["%s:%s" % (ip, port) for ip, port in
|
||||
zip(
|
||||
cls._sanitize_mon_hosts(monitor_ips),
|
||||
monitor_ports)]
|
||||
for monitor in monitors:
|
||||
args += ['--mon_host', monitor]
|
||||
|
||||
if conf:
|
||||
args += ['--conf', conf]
|
||||
|
||||
return args
|
|
@ -21,12 +21,12 @@ from oslo_log import log as logging
|
|||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import fileutils
|
||||
from oslo_utils import netutils
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.i18n import _
|
||||
from os_brick import initiator
|
||||
from os_brick.initiator.connectors import base
|
||||
from os_brick.initiator.connectors import base_rbd
|
||||
from os_brick.initiator import linuxrbd
|
||||
from os_brick.privileged import rbd as rbd_privsep
|
||||
from os_brick import utils
|
||||
|
@ -34,7 +34,7 @@ from os_brick import utils
|
|||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RBDConnector(base.BaseLinuxConnector):
|
||||
class RBDConnector(base_rbd.RBDConnectorMixin, base.BaseLinuxConnector):
|
||||
""""Connector class to attach/detach RBD volumes."""
|
||||
|
||||
def __init__(self, root_helper, driver=None, use_multipath=False,
|
||||
|
@ -65,14 +65,6 @@ class RBDConnector(base.BaseLinuxConnector):
|
|||
# TODO(e0ne): Implement this for local volume.
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_mon_hosts(hosts):
|
||||
def _sanitize_host(host):
|
||||
if netutils.is_valid_ipv6(host):
|
||||
host = '[%s]' % host
|
||||
return host
|
||||
return list(map(_sanitize_host, hosts))
|
||||
|
||||
@staticmethod
|
||||
def _check_or_get_keyring_contents(keyring, cluster_name, user):
|
||||
try:
|
||||
|
@ -143,30 +135,6 @@ class RBDConnector(base.BaseLinuxConnector):
|
|||
|
||||
return rbd_handle
|
||||
|
||||
@classmethod
|
||||
def _get_rbd_args(cls, connection_properties, conf=None):
|
||||
try:
|
||||
user = connection_properties['auth_username']
|
||||
monitor_ips = connection_properties.get('hosts')
|
||||
monitor_ports = connection_properties.get('ports')
|
||||
except KeyError:
|
||||
msg = _("Connect volume failed, malformed connection properties")
|
||||
raise exception.BrickException(msg=msg)
|
||||
|
||||
args = ['--id', user]
|
||||
if monitor_ips and monitor_ports:
|
||||
monitors = ["%s:%s" % (ip, port) for ip, port in
|
||||
zip(
|
||||
cls._sanitize_mon_hosts(monitor_ips),
|
||||
monitor_ports)]
|
||||
for monitor in monitors:
|
||||
args += ['--mon_host', monitor]
|
||||
|
||||
if conf:
|
||||
args += ['--conf', conf]
|
||||
|
||||
return args
|
||||
|
||||
@staticmethod
|
||||
def get_rbd_device_name(pool, volume):
|
||||
"""Return device name which will be generated by RBD kernel module.
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# under the License.
|
||||
|
||||
from os_win import utilsfactory
|
||||
from oslo_concurrency import processutils as putils
|
||||
from oslo_log import log as logging
|
||||
|
||||
from os_brick import exception
|
||||
|
@ -32,6 +33,7 @@ class BaseWindowsConnector(initiator_connector.InitiatorConnector):
|
|||
DEFAULT_DEVICE_SCAN_INTERVAL = 2
|
||||
|
||||
def __init__(self, root_helper=None, *args, **kwargs):
|
||||
kwargs['executor'] = kwargs.get('executor') or putils.execute
|
||||
super(BaseWindowsConnector, self).__init__(root_helper,
|
||||
*args, **kwargs)
|
||||
self.device_scan_interval = kwargs.pop(
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
# Copyright 2020 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 ctypes
|
||||
import errno
|
||||
import json
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import loopingcall
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.i18n import _
|
||||
from os_brick.initiator.connectors import base_rbd
|
||||
from os_brick.initiator.windows import base as win_conn_base
|
||||
from os_brick import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WindowsRBDConnector(base_rbd.RBDConnectorMixin,
|
||||
win_conn_base.BaseWindowsConnector):
|
||||
"""Connector class to attach/detach RBD volumes.
|
||||
|
||||
The Windows RBD connector is very similar to the Linux one.
|
||||
There are a few main differences though:
|
||||
* the Ceph python bindings are not available on Windows yet, so we'll
|
||||
always do a local mount. Besides, Hyper-V cannot use librbd, so
|
||||
we'll need to do a local mount anyway.
|
||||
* The device names aren't handled in the same way. On Windows,
|
||||
disk names such as "\\\\.\\PhysicalDrive1" are provided by the OS and
|
||||
cannot be explicitly requsted.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WindowsRBDConnector, self).__init__(*args, **kwargs)
|
||||
|
||||
self._ensure_rbd_available()
|
||||
|
||||
def _check_rbd(self):
|
||||
cmd = ['where.exe', 'rbd']
|
||||
try:
|
||||
self._execute(*cmd)
|
||||
return True
|
||||
except processutils.ProcessExecutionError:
|
||||
LOG.warning("rbd.exe is not available.")
|
||||
|
||||
return False
|
||||
|
||||
def _ensure_rbd_available(self):
|
||||
if not self._check_rbd():
|
||||
msg = _("rbd.exe is not available.")
|
||||
LOG.error(msg)
|
||||
raise exception.BrickException(msg)
|
||||
|
||||
def get_volume_paths(self, connection_properties):
|
||||
return [self.get_device_name(connection_properties)]
|
||||
|
||||
def _show_rbd_mapping(self, connection_properties):
|
||||
# TODO(lpetrut): consider using "rbd device show" if/when
|
||||
# it becomes available.
|
||||
cmd = ['rbd-wnbd', 'show', connection_properties['name'],
|
||||
'--format', 'json']
|
||||
try:
|
||||
out, err = self._execute(*cmd)
|
||||
return json.loads(out)
|
||||
except processutils.ProcessExecutionError as ex:
|
||||
if abs(ctypes.c_int32(ex.exit_code).value) == errno.ENOENT:
|
||||
LOG.debug("Couldn't find RBD mapping: %s",
|
||||
connection_properties['name'])
|
||||
return
|
||||
raise
|
||||
except json.decoder.JSONDecodeError:
|
||||
msg = _("Could not get rbd mappping.")
|
||||
LOG.exception(msg)
|
||||
raise exception.BrickException(msg)
|
||||
|
||||
def get_device_name(self, connection_properties, expect=True):
|
||||
mapping = self._show_rbd_mapping(connection_properties)
|
||||
if mapping:
|
||||
dev_num = mapping['disk_number']
|
||||
return self._diskutils.get_device_name_by_device_number(dev_num)
|
||||
elif expect:
|
||||
msg = _("The specified RBD image is not mounted: %s")
|
||||
raise exception.VolumeDeviceNotFound(
|
||||
msg % connection_properties['name'])
|
||||
|
||||
def _wait_for_volume(self, connection_properties):
|
||||
"""Wait for the specified volume to become accessible."""
|
||||
attempt = 0
|
||||
dev_path = None
|
||||
|
||||
def _check_rbd_device():
|
||||
rbd_dev_path = self.get_device_name(
|
||||
connection_properties, expect=False)
|
||||
if rbd_dev_path:
|
||||
try:
|
||||
# Under high load, it can take a second before the disk
|
||||
# becomes accessible.
|
||||
with open(rbd_dev_path, 'rb'):
|
||||
pass
|
||||
|
||||
nonlocal dev_path
|
||||
dev_path = rbd_dev_path
|
||||
raise loopingcall.LoopingCallDone()
|
||||
except FileNotFoundError:
|
||||
LOG.debug("The RBD image %(image)s mapped to local device "
|
||||
"%(dev)s isn't available yet.",
|
||||
{'image': connection_properties['name'],
|
||||
'dev': rbd_dev_path})
|
||||
nonlocal attempt
|
||||
attempt += 1
|
||||
if attempt >= self.device_scan_attempts:
|
||||
msg = _("The mounted RBD image isn't available: %s")
|
||||
raise exception.VolumeDeviceNotFound(
|
||||
msg % connection_properties['name'])
|
||||
|
||||
timer = loopingcall.FixedIntervalLoopingCall(_check_rbd_device)
|
||||
timer.start(interval=self.device_scan_interval).wait()
|
||||
return dev_path
|
||||
|
||||
@utils.trace
|
||||
def connect_volume(self, connection_properties):
|
||||
rbd_dev_path = self.get_device_name(connection_properties,
|
||||
expect=False)
|
||||
if not rbd_dev_path:
|
||||
cmd = ['rbd', 'device', 'map', connection_properties['name']]
|
||||
cmd += self._get_rbd_args(connection_properties)
|
||||
self._execute(*cmd)
|
||||
|
||||
rbd_dev_path = self._wait_for_volume(connection_properties)
|
||||
else:
|
||||
LOG.debug('The RBD image %(image)s is already mapped to local '
|
||||
'device %(dev)s',
|
||||
{'image': connection_properties['name'],
|
||||
'dev': rbd_dev_path})
|
||||
|
||||
dev_num = self._diskutils.get_device_number_from_device_name(
|
||||
rbd_dev_path)
|
||||
# TODO(lpetrut): remove this once wnbd honors the SAN policy setting.
|
||||
self._diskutils.set_disk_offline(dev_num)
|
||||
return {'path': rbd_dev_path,
|
||||
'type': 'block'}
|
||||
|
||||
@utils.trace
|
||||
def disconnect_volume(self, connection_properties, device_info=None,
|
||||
force=False, ignore_errors=False):
|
||||
cmd = ['rbd', 'device', 'unmap', connection_properties['name']]
|
||||
cmd += self._get_rbd_args(connection_properties)
|
||||
if force:
|
||||
cmd += ["-o", "hard-disconnect"]
|
||||
self._execute(*cmd)
|
|
@ -0,0 +1,84 @@
|
|||
# Copyright 2020 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.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
|
||||
from os_brick.initiator.connectors import base_rbd
|
||||
from os_brick.tests import base
|
||||
|
||||
|
||||
# Both Linux and Windows tests are using those mocks.
|
||||
class RBDConnectorTestMixin(object):
|
||||
def setUp(self):
|
||||
super(RBDConnectorTestMixin, self).setUp()
|
||||
|
||||
self.user = 'fake_user'
|
||||
self.pool = 'fake_pool'
|
||||
self.volume = 'fake_volume'
|
||||
self.clustername = 'fake_ceph'
|
||||
self.hosts = ['192.168.10.2']
|
||||
self.ports = ['6789']
|
||||
self.keyring = "[client.cinder]\n key = test\n"
|
||||
self.image_name = '%s/%s' % (self.pool, self.volume)
|
||||
|
||||
self.connection_properties = {
|
||||
'auth_username': self.user,
|
||||
'name': self.image_name,
|
||||
'cluster_name': self.clustername,
|
||||
'hosts': self.hosts,
|
||||
'ports': self.ports,
|
||||
'keyring': self.keyring,
|
||||
}
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestRBDConnectorMixin(RBDConnectorTestMixin, base.TestCase):
|
||||
def setUp(self):
|
||||
super(TestRBDConnectorMixin, self).setUp()
|
||||
|
||||
self._conn = base_rbd.RBDConnectorMixin()
|
||||
|
||||
@ddt.data((['192.168.1.1', '192.168.1.2'],
|
||||
['192.168.1.1', '192.168.1.2']),
|
||||
(['3ffe:1900:4545:3:200:f8ff:fe21:67cf',
|
||||
'fe80:0:0:0:200:f8ff:fe21:67cf'],
|
||||
['[3ffe:1900:4545:3:200:f8ff:fe21:67cf]',
|
||||
'[fe80:0:0:0:200:f8ff:fe21:67cf]']),
|
||||
(['foobar', 'fizzbuzz'], ['foobar', 'fizzbuzz']),
|
||||
(['192.168.1.1',
|
||||
'3ffe:1900:4545:3:200:f8ff:fe21:67cf',
|
||||
'hello, world!'],
|
||||
['192.168.1.1',
|
||||
'[3ffe:1900:4545:3:200:f8ff:fe21:67cf]',
|
||||
'hello, world!']))
|
||||
@ddt.unpack
|
||||
def test_sanitize_mon_host(self, hosts_in, hosts_out):
|
||||
self.assertEqual(hosts_out, self._conn._sanitize_mon_hosts(hosts_in))
|
||||
|
||||
def test_get_rbd_args(self):
|
||||
res = self._conn._get_rbd_args(self.connection_properties, None)
|
||||
expected = ['--id', self.user,
|
||||
'--mon_host', self.hosts[0] + ':' + self.ports[0]]
|
||||
self.assertEqual(expected, res)
|
||||
|
||||
def test_get_rbd_args_with_conf(self):
|
||||
res = self._conn._get_rbd_args(self.connection_properties,
|
||||
mock.sentinel.conf_path)
|
||||
expected = ['--id', self.user,
|
||||
'--mon_host', self.hosts[0] + ':' + self.ports[0],
|
||||
'--conf', mock.sentinel.conf_path]
|
||||
self.assertEqual(expected, res)
|
|
@ -19,32 +19,14 @@ from os_brick import exception
|
|||
from os_brick.initiator.connectors import rbd
|
||||
from os_brick.initiator import linuxrbd
|
||||
from os_brick.privileged import rootwrap as priv_rootwrap
|
||||
from os_brick.tests.initiator.connectors import test_base_rbd
|
||||
from os_brick.tests.initiator import test_connector
|
||||
from os_brick import utils
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class RBDConnectorTestCase(test_connector.ConnectorTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(RBDConnectorTestCase, self).setUp()
|
||||
|
||||
self.user = 'fake_user'
|
||||
self.pool = 'fake_pool'
|
||||
self.volume = 'fake_volume'
|
||||
self.clustername = 'fake_ceph'
|
||||
self.hosts = ['192.168.10.2']
|
||||
self.ports = ['6789']
|
||||
self.keyring = "[client.cinder]\n key = test\n"
|
||||
|
||||
self.connection_properties = {
|
||||
'auth_username': self.user,
|
||||
'name': '%s/%s' % (self.pool, self.volume),
|
||||
'cluster_name': self.clustername,
|
||||
'hosts': self.hosts,
|
||||
'ports': self.ports,
|
||||
'keyring': self.keyring,
|
||||
}
|
||||
class RBDConnectorTestCase(test_base_rbd.RBDConnectorTestMixin,
|
||||
test_connector.ConnectorTestCase):
|
||||
|
||||
def test_get_search_path(self):
|
||||
rbd_connector = rbd.RBDConnector(None)
|
||||
|
@ -140,24 +122,6 @@ class RBDConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
conn._check_or_get_keyring_contents, keyring,
|
||||
'cluster', 'user')
|
||||
|
||||
@ddt.data((['192.168.1.1', '192.168.1.2'],
|
||||
['192.168.1.1', '192.168.1.2']),
|
||||
(['3ffe:1900:4545:3:200:f8ff:fe21:67cf',
|
||||
'fe80:0:0:0:200:f8ff:fe21:67cf'],
|
||||
['[3ffe:1900:4545:3:200:f8ff:fe21:67cf]',
|
||||
'[fe80:0:0:0:200:f8ff:fe21:67cf]']),
|
||||
(['foobar', 'fizzbuzz'], ['foobar', 'fizzbuzz']),
|
||||
(['192.168.1.1',
|
||||
'3ffe:1900:4545:3:200:f8ff:fe21:67cf',
|
||||
'hello, world!'],
|
||||
['192.168.1.1',
|
||||
'[3ffe:1900:4545:3:200:f8ff:fe21:67cf]',
|
||||
'hello, world!']))
|
||||
@ddt.unpack
|
||||
def test_sanitize_mon_host(self, hosts_in, hosts_out):
|
||||
conn = rbd.RBDConnector(None)
|
||||
self.assertEqual(hosts_out, conn._sanitize_mon_hosts(hosts_in))
|
||||
|
||||
@mock.patch('os_brick.initiator.connectors.rbd.tempfile.mkstemp')
|
||||
def test_create_ceph_conf(self, mock_mkstemp):
|
||||
mockopen = mock.mock_open()
|
||||
|
@ -251,16 +215,6 @@ class RBDConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
'type': 'block'}
|
||||
self.assertEqual(expected_info, device_info)
|
||||
|
||||
@mock.patch.object(priv_rootwrap, 'execute', return_value=None)
|
||||
def test_connect_local_volume_without_auth(self, mock_execute):
|
||||
rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
|
||||
conn = {'name': 'pool/image',
|
||||
'hosts': ['192.168.10.2'],
|
||||
'ports': ['6789']}
|
||||
self.assertRaises(exception.BrickException,
|
||||
rbd_connector.connect_volume,
|
||||
conn)
|
||||
|
||||
@mock.patch('os_brick.initiator.connectors.rbd.'
|
||||
'RBDConnector._local_attach_volume')
|
||||
def test_connect_volume_local(self, mock_local_attach):
|
||||
|
@ -489,20 +443,6 @@ class RBDConnectorTestCase(test_connector.ConnectorTestCase):
|
|||
mock_delete.assert_called_once_with(mock_config.return_value)
|
||||
mock_open.assert_not_called()
|
||||
|
||||
def test__get_rbd_args(self):
|
||||
res = rbd.RBDConnector._get_rbd_args(self.connection_properties, None)
|
||||
expected = ['--id', self.user,
|
||||
'--mon_host', self.hosts[0] + ':' + self.ports[0]]
|
||||
self.assertEqual(expected, res)
|
||||
|
||||
def test__get_rbd_args_with_conf(self):
|
||||
res = rbd.RBDConnector._get_rbd_args(self.connection_properties,
|
||||
mock.sentinel.conf_path)
|
||||
expected = ['--id', self.user,
|
||||
'--mon_host', self.hosts[0] + ':' + self.ports[0],
|
||||
'--conf', mock.sentinel.conf_path]
|
||||
self.assertEqual(expected, res)
|
||||
|
||||
@mock.patch.object(rbd.RBDConnector, '_get_rbd_args')
|
||||
@mock.patch.object(rbd.RBDConnector, '_execute')
|
||||
def test_find_root_device(self, mock_execute, mock_args):
|
||||
|
|
|
@ -195,7 +195,7 @@ class ConnectorTestCase(test_base.TestCase):
|
|||
def test_get_connector_mapping_win32(self):
|
||||
mapping_win32 = connector.get_connector_mapping()
|
||||
self.assertTrue('ISCSI' in mapping_win32)
|
||||
self.assertFalse('RBD' in mapping_win32)
|
||||
self.assertTrue('RBD' in mapping_win32)
|
||||
self.assertFalse('STORPOOL' in mapping_win32)
|
||||
|
||||
@mock.patch('os_brick.initiator.connector.platform.machine')
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
# Copyright 2020 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.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
from oslo_concurrency import processutils
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.initiator.windows import rbd
|
||||
from os_brick.tests.initiator.connectors import test_base_rbd
|
||||
from os_brick.tests.windows import test_base
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class WindowsRBDConnectorTestCase(test_base_rbd.RBDConnectorTestMixin,
|
||||
test_base.WindowsConnectorTestBase):
|
||||
def setUp(self):
|
||||
super(WindowsRBDConnectorTestCase, self).setUp()
|
||||
|
||||
self._diskutils = mock.Mock()
|
||||
self._execute = mock.Mock(return_value=['fake_stdout', 'fake_stderr'])
|
||||
|
||||
self._conn = rbd.WindowsRBDConnector(execute=self._execute)
|
||||
self._conn._diskutils = self._diskutils
|
||||
|
||||
self.dev_name = '\\\\.\\PhysicalDrive5'
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_check_rbd(self, rbd_available):
|
||||
self._execute.side_effect = (
|
||||
None if rbd_available
|
||||
else processutils.ProcessExecutionError)
|
||||
|
||||
self.assertEqual(rbd_available, self._conn._check_rbd())
|
||||
|
||||
if rbd_available:
|
||||
self._conn._ensure_rbd_available()
|
||||
else:
|
||||
self.assertRaises(exception.BrickException,
|
||||
self._conn._ensure_rbd_available)
|
||||
|
||||
expected_cmd = ['where.exe', 'rbd']
|
||||
self._execute.assert_any_call(*expected_cmd)
|
||||
|
||||
@mock.patch.object(rbd.WindowsRBDConnector, 'get_device_name')
|
||||
def test_get_volume_paths(self, mock_get_dev_name):
|
||||
vol_paths = self._conn.get_volume_paths(mock.sentinel.conn_props)
|
||||
self.assertEqual([mock_get_dev_name.return_value], vol_paths)
|
||||
|
||||
mock_get_dev_name.assert_called_once_with(mock.sentinel.conn_props)
|
||||
|
||||
@ddt.data(True, False)
|
||||
@mock.patch.object(rbd.WindowsRBDConnector, 'get_device_name')
|
||||
@mock.patch('oslo_utils.eventletutils.EventletEvent.wait')
|
||||
def test_wait_for_volume(self, device_found, mock_wait, mock_get_dev_name):
|
||||
mock_open = mock.mock_open()
|
||||
if device_found:
|
||||
mock_get_dev_name.return_value = mock.sentinel.dev_name
|
||||
else:
|
||||
# First call fails to locate the device, the following ones can't
|
||||
# open it.
|
||||
mock_get_dev_name.side_effect = (
|
||||
[None] +
|
||||
[mock.sentinel.dev_name] * self._conn.device_scan_attempts)
|
||||
mock_open.side_effect = FileNotFoundError
|
||||
|
||||
with mock.patch.object(rbd, 'open', mock_open,
|
||||
create=True):
|
||||
if device_found:
|
||||
dev_name = self._conn._wait_for_volume(
|
||||
self.connection_properties)
|
||||
self.assertEqual(mock.sentinel.dev_name, dev_name)
|
||||
else:
|
||||
self.assertRaises(exception.VolumeDeviceNotFound,
|
||||
self._conn._wait_for_volume,
|
||||
self.connection_properties)
|
||||
|
||||
mock_open.assert_any_call(mock.sentinel.dev_name, 'rb')
|
||||
mock_get_dev_name.assert_any_call(self.connection_properties,
|
||||
expect=False)
|
||||
|
||||
@mock.patch.object(rbd.WindowsRBDConnector, '_wait_for_volume')
|
||||
@mock.patch.object(rbd.WindowsRBDConnector, 'get_device_name')
|
||||
def test_connect_volume(self, mock_get_dev_name, mock_wait_vol):
|
||||
mock_get_dev_name.return_value = None
|
||||
mock_wait_vol.return_value = self.dev_name
|
||||
|
||||
ret_val = self._conn.connect_volume(self.connection_properties)
|
||||
exp_ret_val = {
|
||||
'path': self.dev_name,
|
||||
'type': 'block'
|
||||
}
|
||||
self.assertEqual(exp_ret_val, ret_val)
|
||||
|
||||
exp_exec_args = ['rbd', 'device', 'map', self.image_name]
|
||||
exp_exec_args += self._conn._get_rbd_args(self.connection_properties)
|
||||
self._execute.assert_any_call(*exp_exec_args)
|
||||
|
||||
mock_wait_vol.assert_called_once_with(self.connection_properties)
|
||||
mock_get_dev_num = self._diskutils.get_device_number_from_device_name
|
||||
mock_get_dev_num.assert_called_once_with(self.dev_name)
|
||||
self._diskutils.set_disk_offline.assert_called_once_with(
|
||||
mock_get_dev_num.return_value)
|
||||
|
||||
@ddt.data(True, False)
|
||||
@mock.patch.object(rbd.WindowsRBDConnector, 'get_device_name')
|
||||
def test_disconnect_volume(self, force, mock_get_dev_name):
|
||||
mock_get_dev_name.return_value = self.dev_name
|
||||
|
||||
self._conn.disconnect_volume(self.connection_properties, force=force)
|
||||
|
||||
exp_exec_args = ['rbd', 'device', 'unmap', self.image_name]
|
||||
exp_exec_args += self._conn._get_rbd_args(self.connection_properties)
|
||||
if force:
|
||||
exp_exec_args += ["-o", "hard-disconnect"]
|
||||
|
||||
self._execute.assert_any_call(*exp_exec_args)
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
RBD volumes can now be attached to Windows hosts and Hyper-V VMs.
|
||||
The minimum requirements are Ceph 16 (Pacific) and Windows Server 2016.
|
Loading…
Reference in New Issue