From e6cdd1693ba843e1c8dcccbde3af20f62eb0b5a2 Mon Sep 17 00:00:00 2001 From: "Walter A. Boring IV" Date: Mon, 20 Apr 2015 13:42:44 -0700 Subject: [PATCH] Switch to using os-brick This patch changes the internals of some of the libvirt volume drivers to use the os-brick Connector objects. Cinder already uses os-brick for volume discovery and removal for copy volume to image and image to volume operations. This patch changes the following libvirt volume drivers: LibvirtISCSIVolumeDriver LibvirtISERVolumeDriver LibvirtAOEVolumeDriver LibvirtFibreChannelVolumeDriver This patch also removes the need to have the nova/storage module that was used by the above listed libvirt volume drivers. This patch also fetches the initiator side information from os-brick. This replaces the internals of the libvirt driver's get_volume_connector Also updated the rootwrap filters to consolidate them under a single comment, and added a new os-brick needed command. blueprint use-os-brick-library Change-Id: I400db60fcc29c2d5e2d3b9dabc055649138468eb Depends-On: Id36f9665c8ff2a720713ceaaa5b05f9b03706681 --- etc/nova/rootwrap.d/compute.filters | 12 +- nova/storage/__init__.py | 0 nova/storage/linuxscsi.py | 164 ---- nova/tests/unit/test_linuxscsi.py | 175 ---- .../unit/virt/libvirt/fake_libvirt_utils.py | 58 -- .../virt/libvirt/fake_os_brick_connector.py | 42 + nova/tests/unit/virt/libvirt/test_driver.py | 18 +- nova/tests/unit/virt/libvirt/test_utils.py | 62 -- nova/tests/unit/virt/libvirt/test_volume.py | 816 +--------------- nova/tests/unit/virt/test_virt_drivers.py | 9 +- nova/tests/unit/virt/test_volumeutils.py | 29 +- nova/virt/libvirt/driver.py | 40 +- nova/virt/libvirt/utils.py | 155 ---- nova/virt/libvirt/volume.py | 871 ++---------------- nova/virt/volumeutils.py | 23 +- requirements.txt | 1 + 16 files changed, 196 insertions(+), 2279 deletions(-) delete mode 100644 nova/storage/__init__.py delete mode 100644 nova/storage/linuxscsi.py delete mode 100644 nova/tests/unit/test_linuxscsi.py create mode 100644 nova/tests/unit/virt/libvirt/fake_os_brick_connector.py diff --git a/etc/nova/rootwrap.d/compute.filters b/etc/nova/rootwrap.d/compute.filters index 37d1c2d503..968391be84 100644 --- a/etc/nova/rootwrap.d/compute.filters +++ b/etc/nova/rootwrap.d/compute.filters @@ -180,9 +180,6 @@ mkfs.ext3: CommandFilter, mkfs.ext3, root mkfs.ext4: CommandFilter, mkfs.ext4, root mkfs.ntfs: CommandFilter, mkfs.ntfs, root -# nova/virt/libvirt/connection.py: -read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi - # nova/virt/libvirt/connection.py: lvremove: CommandFilter, lvremove, root @@ -203,13 +200,12 @@ tgtadm: CommandFilter, tgtadm, root read_passwd: RegExpFilter, cat, root, cat, (/var|/usr)?/tmp/openstack-vfs-localfs[^/]+/etc/passwd read_shadow: RegExpFilter, cat, root, cat, (/var|/usr)?/tmp/openstack-vfs-localfs[^/]+/etc/shadow -# nova/virt/libvirt/volume.py: 'multipath' '-R' +# os-brick needed commands +read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi multipath: CommandFilter, multipath, root - -# nova/virt/libvirt/utils.py: +# multipathd show status +multipathd: CommandFilter, multipathd, root systool: CommandFilter, systool, root - -# nova/storage/linuxscsi.py: sginfo -r sginfo: CommandFilter, sginfo, root # nova/storage/linuxscsi.py: sg_scan device diff --git a/nova/storage/__init__.py b/nova/storage/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/nova/storage/linuxscsi.py b/nova/storage/linuxscsi.py deleted file mode 100644 index 214f4a6585..0000000000 --- a/nova/storage/linuxscsi.py +++ /dev/null @@ -1,164 +0,0 @@ -# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. -# -# 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. - -"""Generic linux scsi subsystem utilities.""" - -from oslo_concurrency import processutils -from oslo_log import log as logging -from oslo_service import loopingcall - -from nova.i18n import _LW -from nova import utils - -import os -import re - -LOG = logging.getLogger(__name__) - -MULTIPATH_WWID_REGEX = re.compile("\((?P.+)\)") - - -def echo_scsi_command(path, content): - """Used to echo strings to scsi subsystem.""" - args = ["-a", path] - kwargs = dict(process_input=content, run_as_root=True) - utils.execute('tee', *args, **kwargs) - - -def rescan_hosts(hbas): - for hba in hbas: - echo_scsi_command("/sys/class/scsi_host/%s/scan" - % hba['host_device'], "- - -") - - -def get_device_list(): - (out, err) = utils.execute('sginfo', '-r', run_as_root=True) - devices = [] - if out: - line = out.strip() - devices = line.split(" ") - - return devices - - -def get_device_info(device): - (out, err) = utils.execute('sg_scan', device, run_as_root=True) - dev_info = {'device': device, 'host': None, - 'channel': None, 'id': None, 'lun': None} - if out: - line = out.strip() - line = line.replace(device + ": ", "") - info = line.split(" ") - - for item in info: - if '=' in item: - pair = item.split('=') - dev_info[pair[0]] = pair[1] - elif 'scsi' in item: - dev_info['host'] = item.replace('scsi', '') - - return dev_info - - -def _wait_for_remove(device, tries): - tries = tries + 1 - LOG.debug("Trying (%(tries)s) to remove device %(device)s", - {'tries': tries, 'device': device["device"]}) - - path = "/sys/bus/scsi/drivers/sd/%s:%s:%s:%s/delete" - echo_scsi_command(path % (device["host"], device["channel"], - device["id"], device["lun"]), - "1") - - devices = get_device_list() - if device["device"] not in devices: - raise loopingcall.LoopingCallDone() - - -def remove_device(device): - tries = 0 - timer = loopingcall.FixedIntervalLoopingCall(_wait_for_remove, device, - tries) - timer.start(interval=2).wait() - timer.stop() - - -def find_multipath_device(device): - """Try and discover the multipath device for a volume.""" - mdev = None - devices = [] - out = None - try: - (out, err) = utils.execute('multipath', '-l', device, - run_as_root=True) - except processutils.ProcessExecutionError as exc: - LOG.warning(_LW("Multipath call failed exit (%(code)s)"), - {'code': exc.exit_code}) - return None - - if out: - lines = out.strip() - lines = lines.split("\n") - if lines: - - # Use the device name, be it the WWID, mpathN or custom alias of - # a device to build the device path. This should be the first item - # on the first line of output from `multipath -l /dev/${path}`. - mdev_name = lines[0].split(" ")[0] - mdev = '/dev/mapper/%s' % mdev_name - - # Find the WWID for the LUN if we are using mpathN or aliases. - wwid_search = MULTIPATH_WWID_REGEX.search(lines[0]) - if wwid_search is not None: - mdev_id = wwid_search.group('wwid') - else: - mdev_id = mdev_name - - # Confirm that the device is present. - try: - os.stat(mdev) - except OSError: - LOG.warning(_LW("Couldn't find multipath device %s"), mdev) - return None - - LOG.debug("Found multipath device = %s", mdev) - - device_lines = lines[3:] - for dev_line in device_lines: - if dev_line.find("policy") != -1: - continue - if '#' in dev_line: - LOG.warning(_LW('Skip faulty line "%(dev_line)s" of' - ' multipath device %(mdev)s'), - {'mdev': mdev, 'dev_line': dev_line}) - continue - - dev_line = dev_line.lstrip(' |-`') - dev_info = dev_line.split() - address = dev_info[0].split(":") - - dev = {'device': '/dev/%s' % dev_info[1], - 'host': address[0], 'channel': address[1], - 'id': address[2], 'lun': address[3] - } - - devices.append(dev) - - if mdev is not None: - info = {"device": mdev, - "id": mdev_id, - "name": mdev_name, - "devices": devices} - return info - return None diff --git a/nova/tests/unit/test_linuxscsi.py b/nova/tests/unit/test_linuxscsi.py deleted file mode 100644 index 279b3c9a6a..0000000000 --- a/nova/tests/unit/test_linuxscsi.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright 2010 OpenStack Foundation -# (c) Copyright 2012-2013 Hewlett-Packard Development Company, L.P. -# -# 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_config import cfg -from oslo_log import log as logging - -from nova.storage import linuxscsi -from nova import test -from nova import utils - -import os - -LOG = logging.getLogger(__name__) - -CONF = cfg.CONF - - -class StorageLinuxSCSITestCase(test.NoDBTestCase): - def setUp(self): - super(StorageLinuxSCSITestCase, self).setUp() - self.executes = [] - self.fake_stat_result = os.stat(__file__) - - def fake_execute(*cmd, **kwargs): - self.executes.append(cmd) - return None, None - - def fake_stat(path): - return self.fake_stat_result - - self.stubs.Set(utils, 'execute', fake_execute) - self.stubs.Set(os, 'stat', fake_stat) - - def test_find_multipath_device_3par(self): - def fake_execute(*cmd, **kwargs): - out = ("350002ac20398383d dm-3 3PARdata,VV\n" - "size=2.0G features='0' hwhandler='0' wp=rw\n" - "`-+- policy='round-robin 0' prio=-1 status=active\n" - " |- 0:0:0:1 sde 8:64 active undef running\n" - " `- 2:0:0:1 sdf 8:80 active undef running\n" - ) - return out, None - - self.stubs.Set(utils, 'execute', fake_execute) - - info = linuxscsi.find_multipath_device('/dev/sde') - LOG.error("info = %s" % info) - - self.assertEqual("350002ac20398383d", info["name"]) - self.assertEqual("350002ac20398383d", info["id"]) - self.assertEqual("/dev/mapper/350002ac20398383d", info["device"]) - - self.assertEqual("/dev/sde", info['devices'][0]['device']) - self.assertEqual("0", info['devices'][0]['host']) - self.assertEqual("0", info['devices'][0]['id']) - self.assertEqual("0", info['devices'][0]['channel']) - self.assertEqual("1", info['devices'][0]['lun']) - - self.assertEqual("/dev/sdf", info['devices'][1]['device']) - self.assertEqual("2", info['devices'][1]['host']) - self.assertEqual("0", info['devices'][1]['id']) - self.assertEqual("0", info['devices'][1]['channel']) - self.assertEqual("1", info['devices'][1]['lun']) - - def test_find_multipath_device_3par_ufn(self): - def fake_execute(*cmd, **kwargs): - out = ("mpath6 (350002ac20398383d) dm-3 3PARdata,VV\n" - "size=2.0G features='0' hwhandler='0' wp=rw\n" - "`-+- policy='round-robin 0' prio=-1 status=active\n" - " |- 0:0:0:1 sde 8:64 active undef running\n" - " `- 2:0:0:1 sdf 8:80 active undef running\n" - ) - return out, None - - self.stubs.Set(utils, 'execute', fake_execute) - - info = linuxscsi.find_multipath_device('/dev/sde') - LOG.error("info = %s" % info) - - self.assertEqual("mpath6", info["name"]) - self.assertEqual("350002ac20398383d", info["id"]) - self.assertEqual("/dev/mapper/mpath6", info["device"]) - - self.assertEqual("/dev/sde", info['devices'][0]['device']) - self.assertEqual("0", info['devices'][0]['host']) - self.assertEqual("0", info['devices'][0]['id']) - self.assertEqual("0", info['devices'][0]['channel']) - self.assertEqual("1", info['devices'][0]['lun']) - - self.assertEqual("/dev/sdf", info['devices'][1]['device']) - self.assertEqual("2", info['devices'][1]['host']) - self.assertEqual("0", info['devices'][1]['id']) - self.assertEqual("0", info['devices'][1]['channel']) - self.assertEqual("1", info['devices'][1]['lun']) - - def test_find_multipath_device_svc(self): - def fake_execute(*cmd, **kwargs): - out = ("36005076da00638089c000000000004d5 dm-2 IBM,2145\n" - "size=954M features='1 queue_if_no_path' hwhandler='0'" - " wp=rw\n" - "|-+- policy='round-robin 0' prio=-1 status=active\n" - "| |- 6:0:2:0 sde 8:64 active undef running\n" - "| `- 6:0:4:0 sdg 8:96 active undef running\n" - "`-+- policy='round-robin 0' prio=-1 status=enabled\n" - " |- 6:0:3:0 sdf 8:80 active undef running\n" - " `- 6:0:5:0 sdh 8:112 active undef running\n" - ) - return out, None - - self.stubs.Set(utils, 'execute', fake_execute) - - info = linuxscsi.find_multipath_device('/dev/sde') - LOG.error("info = %s" % info) - - self.assertEqual("36005076da00638089c000000000004d5", info["name"]) - self.assertEqual("36005076da00638089c000000000004d5", info["id"]) - self.assertEqual("/dev/mapper/36005076da00638089c000000000004d5", - info["device"]) - - self.assertEqual("/dev/sde", info['devices'][0]['device']) - self.assertEqual("6", info['devices'][0]['host']) - self.assertEqual("0", info['devices'][0]['channel']) - self.assertEqual("2", info['devices'][0]['id']) - self.assertEqual("0", info['devices'][0]['lun']) - - self.assertEqual("/dev/sdf", info['devices'][2]['device']) - self.assertEqual("6", info['devices'][2]['host']) - self.assertEqual("0", info['devices'][2]['channel']) - self.assertEqual("3", info['devices'][2]['id']) - self.assertEqual("0", info['devices'][2]['lun']) - - def test_find_multipath_device_ds8000(self): - def fake_execute(*cmd, **kwargs): - out = ("36005076303ffc48e0000000000000101 dm-2 IBM,2107900\n" - "size=1.0G features='1 queue_if_no_path' hwhandler='0'" - " wp=rw\n" - "`-+- policy='round-robin 0' prio=-1 status=active\n" - " |- 6:0:2:0 sdd 8:64 active undef running\n" - " `- 6:1:0:3 sdc 8:32 active undef running\n" - ) - return out, None - - self.stubs.Set(utils, 'execute', fake_execute) - - info = linuxscsi.find_multipath_device('/dev/sdd') - LOG.error("info = %s" % info) - - self.assertEqual("36005076303ffc48e0000000000000101", info["name"]) - self.assertEqual("36005076303ffc48e0000000000000101", info["id"]) - self.assertEqual("/dev/mapper/36005076303ffc48e0000000000000101", - info["device"]) - - self.assertEqual("/dev/sdd", info['devices'][0]['device']) - self.assertEqual("6", info['devices'][0]['host']) - self.assertEqual("0", info['devices'][0]['channel']) - self.assertEqual("2", info['devices'][0]['id']) - self.assertEqual("0", info['devices'][0]['lun']) - - self.assertEqual("/dev/sdc", info['devices'][1]['device']) - self.assertEqual("6", info['devices'][1]['host']) - self.assertEqual("1", info['devices'][1]['channel']) - self.assertEqual("0", info['devices'][1]['id']) - self.assertEqual("3", info['devices'][1]['lun']) diff --git a/nova/tests/unit/virt/libvirt/fake_libvirt_utils.py b/nova/tests/unit/virt/libvirt/fake_libvirt_utils.py index d6dc0caae5..fa81a9e29e 100644 --- a/nova/tests/unit/virt/libvirt/fake_libvirt_utils.py +++ b/nova/tests/unit/virt/libvirt/fake_libvirt_utils.py @@ -24,64 +24,6 @@ disk_backing_files = {} disk_type = "qcow2" -def get_iscsi_initiator(): - return "fake.initiator.iqn" - - -def get_fc_hbas(): - return [{'ClassDevice': 'host1', - 'ClassDevicePath': '/sys/devices/pci0000:00/0000:00:03.0' - '/0000:05:00.2/host1/fc_host/host1', - 'dev_loss_tmo': '30', - 'fabric_name': '0x1000000533f55566', - 'issue_lip': '', - 'max_npiv_vports': '255', - 'maxframe_size': '2048 bytes', - 'node_name': '0x200010604b019419', - 'npiv_vports_inuse': '0', - 'port_id': '0x680409', - 'port_name': '0x100010604b019419', - 'port_state': 'Online', - 'port_type': 'NPort (fabric via point-to-point)', - 'speed': '10 Gbit', - 'supported_classes': 'Class 3', - 'supported_speeds': '10 Gbit', - 'symbolic_name': 'Emulex 554M FV4.0.493.0 DV8.3.27', - 'tgtid_bind_type': 'wwpn (World Wide Port Name)', - 'uevent': None, - 'vport_create': '', - 'vport_delete': ''}] - - -def get_fc_hbas_info(): - hbas = get_fc_hbas() - info = [{'port_name': hbas[0]['port_name'].replace('0x', ''), - 'node_name': hbas[0]['node_name'].replace('0x', ''), - 'host_device': hbas[0]['ClassDevice'], - 'device_path': hbas[0]['ClassDevicePath']}] - return info - - -def get_fc_wwpns(): - hbas = get_fc_hbas() - wwpns = [] - for hba in hbas: - wwpn = hba['port_name'].replace('0x', '') - wwpns.append(wwpn) - - return wwpns - - -def get_fc_wwnns(): - hbas = get_fc_hbas() - wwnns = [] - for hba in hbas: - wwnn = hba['node_name'].replace('0x', '') - wwnns.append(wwnn) - - return wwnns - - def create_image(disk_format, path, size): pass diff --git a/nova/tests/unit/virt/libvirt/fake_os_brick_connector.py b/nova/tests/unit/virt/libvirt/fake_os_brick_connector.py new file mode 100644 index 0000000000..710b199075 --- /dev/null +++ b/nova/tests/unit/virt/libvirt/fake_os_brick_connector.py @@ -0,0 +1,42 @@ +# 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. + + +def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath, + host=None): + """Fake os-brick.""" + + props = {} + props['ip'] = my_ip + props['host'] = host + iscsi = ISCSIConnector('') + props['initiator'] = iscsi.get_initiator() + props['wwpns'] = ['100010604b019419'] + props['wwnns'] = ['200010604b019419'] + props['multipath'] = multipath + props['platform'] = 'x86_64' + props['os_type'] = 'linux2' + return props + + +class ISCSIConnector(object): + """Mimick the iSCSI connector.""" + + def __init__(self, root_helper, driver=None, + execute=None, use_multipath=False, + device_scan_attempts=3, + *args, **kwargs): + self.root_herlp = root_helper, + self.execute = execute + + def get_initiator(self): + return "fake_iscsi.iqn" diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index bdceebc105..a1ac7c6476 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -33,6 +33,7 @@ import fixtures from lxml import etree import mock from mox3 import mox +from os_brick.initiator import connector from oslo_concurrency import lockutils from oslo_concurrency import processutils from oslo_config import cfg @@ -105,6 +106,7 @@ CONF.import_opt('host', 'nova.netconf') CONF.import_opt('my_ip', 'nova.netconf') CONF.import_opt('image_cache_subdirectory_name', 'nova.virt.imagecache') CONF.import_opt('instances_path', 'nova.compute.manager') +CONF.import_opt('iscsi_use_multipath', 'nova.virt.libvirt.volume', 'libvirt') _fake_network_info = fake_network.fake_get_instance_nw_info @@ -864,7 +866,8 @@ class LibvirtConnTestCase(test.NoDBTestCase): self.assertRaises(exception.PciDeviceDetachFailed, drvr._detach_pci_devices, guest, pci_devices) - def test_get_connector(self): + @mock.patch.object(connector, 'get_connector_properties') + def test_get_connector(self, fake_get_connector): initiator = 'fake.initiator.iqn' ip = 'fakeip' host = 'fakehost' @@ -873,7 +876,6 @@ class LibvirtConnTestCase(test.NoDBTestCase): self.flags(my_ip=ip) self.flags(host=host) - drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) expected = { 'ip': ip, 'initiator': initiator, @@ -884,16 +886,26 @@ class LibvirtConnTestCase(test.NoDBTestCase): volume = { 'id': 'fake' } + + # TODO(walter-boring) add the fake in os-brick + fake_get_connector.return_value = expected + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) result = drvr.get_volume_connector(volume) self.assertThat(expected, matchers.DictMatches(result)) - def test_get_connector_storage_ip(self): + @mock.patch.object(connector, 'get_connector_properties') + def test_get_connector_storage_ip(self, fake_get_connector): ip = '100.100.100.100' storage_ip = '101.101.101.101' self.flags(my_block_storage_ip=storage_ip, my_ip=ip) volume = { 'id': 'fake' } + expected = { + 'ip': storage_ip + } + # TODO(walter-boring) add the fake in os-brick + fake_get_connector.return_value = expected drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True) result = drvr.get_volume_connector(volume) self.assertEqual(storage_ip, result['ip']) diff --git a/nova/tests/unit/virt/libvirt/test_utils.py b/nova/tests/unit/virt/libvirt/test_utils.py index 944d4b1d17..95a3887ab0 100644 --- a/nova/tests/unit/virt/libvirt/test_utils.py +++ b/nova/tests/unit/virt/libvirt/test_utils.py @@ -25,7 +25,6 @@ import six from nova.compute import arch from nova import exception -from nova.storage import linuxscsi from nova import test from nova.tests.unit import fake_instance from nova import utils @@ -750,64 +749,3 @@ disk size: 4.4M image_meta = {'properties': {'architecture': "X86_64"}} image_arch = libvirt_utils.get_arch(image_meta) self.assertEqual(arch.X86_64, image_arch) - - @mock.patch.object(linuxscsi, 'echo_scsi_command') - def test_perform_unit_add_for_s390(self, mock_execute): - device_number = "0.0.2319" - target_wwn = "0x50014380242b9751" - lun = 1 - libvirt_utils.perform_unit_add_for_s390(device_number, target_wwn, lun) - - mock_execute.assert_called_once_with( - '/sys/bus/ccw/drivers/zfcp/0.0.2319/0x50014380242b9751/unit_add', - lun) - - @mock.patch.object(libvirt_utils.LOG, 'warn') - @mock.patch.object(linuxscsi, 'echo_scsi_command') - def test_perform_unit_add_for_s390_failed(self, mock_execute, mock_warn): - mock_execute.side_effect = processutils.ProcessExecutionError( - exit_code=1, stderr='oops') - device_number = "0.0.2319" - target_wwn = "0x50014380242b9751" - lun = 1 - libvirt_utils.perform_unit_add_for_s390(device_number, target_wwn, lun) - - mock_execute.assert_called_once_with( - '/sys/bus/ccw/drivers/zfcp/0.0.2319/0x50014380242b9751/unit_add', - lun) - # NOTE(mriedem): A better test is to probably make sure that the stderr - # message is logged in the warning but that gets messy with Message - # objects and mock.call_args. - self.assertEqual(1, mock_warn.call_count) - - @mock.patch.object(linuxscsi, 'echo_scsi_command') - def test_perform_unit_remove_for_s390(self, mock_execute): - device_number = "0.0.2319" - target_wwn = "0x50014380242b9751" - lun = 1 - libvirt_utils.perform_unit_remove_for_s390(device_number, - target_wwn, lun) - - mock_execute.assert_called_once_with( - '/sys/bus/ccw/drivers/zfcp/' - '0.0.2319/0x50014380242b9751/unit_remove', lun) - - @mock.patch.object(libvirt_utils.LOG, 'warn') - @mock.patch.object(linuxscsi, 'echo_scsi_command') - def test_perform_unit_remove_for_s390_failed(self, mock_execute, - mock_warn): - mock_execute.side_effect = processutils.ProcessExecutionError( - exit_code=1, stderr='oops') - device_number = "0.0.2319" - target_wwn = "0x50014380242b9751" - lun = 1 - libvirt_utils.perform_unit_remove_for_s390(device_number, - target_wwn, lun) - - mock_execute.assert_called_once_with( - '/sys/bus/ccw/drivers/zfcp/' - '0.0.2319/0x50014380242b9751/unit_remove', lun) - # NOTE(mriedem): A better test is to probably make sure that the stderr - # message is logged in the warning but that gets messy with Message - # objects and mock.call_args. - self.assertEqual(1, mock_warn.call_count) diff --git a/nova/tests/unit/virt/libvirt/test_volume.py b/nova/tests/unit/virt/libvirt/test_volume.py index bb2f009356..5c1f8e407a 100644 --- a/nova/tests/unit/virt/libvirt/test_volume.py +++ b/nova/tests/unit/virt/libvirt/test_volume.py @@ -13,24 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. -import contextlib -import glob import os import platform -import time -import eventlet import fixtures import mock +from os_brick.initiator import connector from oslo_concurrency import processutils from oslo_config import cfg -import six from nova.compute import arch from nova import exception -from nova.storage import linuxscsi from nova import test -from nova.tests.unit.virt.libvirt import fake_libvirt_utils from nova.tests.unit.virt.libvirt import fakelibvirt from nova import utils from nova.virt.libvirt import host @@ -314,12 +308,6 @@ class LibvirtVolumeTestCase(test.NoDBTestCase): fake_dev_path = "/dev/disk/by-path/" + dev_format return fake_dev_path - def test_rescan_multipath(self): - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - libvirt_driver._rescan_multipath() - expected_multipath_cmd = ('multipath', '-r') - self.assertIn(expected_multipath_cmd, self.executes) - def test_iscsiadm_discover_parsing(self): # Ensure that parsing iscsiadm discover ignores cruft. @@ -340,223 +328,35 @@ Setting up iSCSI targets: unused %s %s """ % (targets[0][0], targets[0][1], targets[1][0], targets[1][1]) driver = volume.LibvirtISCSIVolumeDriver("none") - out = driver._get_target_portals_from_iscsiadm_output(sample_input) + out = driver.connector._get_target_portals_from_iscsiadm_output( + sample_input) self.assertEqual(out, targets) - def test_libvirt_iscsi_get_host_device(self, transport=None): - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - connection_info = self.iscsi_connection(self.vol, self.location, - self.iqn) - iscsi_properties = connection_info['data'] - expected_device = self.generate_device(transport, 1, False) - if transport: - self.stubs.Set(glob, 'glob', lambda x: [expected_device]) - self.stubs.Set(libvirt_driver, '_validate_transport', - lambda x: True) - device = libvirt_driver._get_host_device(iscsi_properties) - self.assertEqual(expected_device, device) - - def test_libvirt_iscsi_get_host_device_with_transport(self): - self.flags(iscsi_iface='fake_transport', group='libvirt') - self.test_libvirt_iscsi_get_host_device('fake_transport') - - @mock.patch.object(volume.utils, 'execute') - def test_libvirt_iscsi_validate_transport(self, mock_execute): - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - sample_output = ('# BEGIN RECORD 2.0-872\n' - 'iface.iscsi_ifacename = %s.fake_suffix\n' - 'iface.net_ifacename = \n' - 'iface.ipaddress = \n' - 'iface.hwaddress = 00:53:00:00:53:00\n' - 'iface.transport_name = %s\n' - 'iface.initiatorname = \n' - '# END RECORD') - for tport in libvirt_driver.supported_transports: - mock_execute.return_value = (sample_output % (tport, tport), '') - self.assertTrue(libvirt_driver._validate_transport( - tport + '.fake_suffix')) - - mock_execute.return_value = ("", 'iscsiadm: Could not ' - 'read iface fake_transport (6)') - self.assertFalse(libvirt_driver._validate_transport('fake_transport')) - def test_libvirt_iscsi_driver(self, transport=None): - # NOTE(vish) exists is to make driver assume connecting worked - self.stubs.Set(os.path, 'exists', lambda x: True) libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - connection_info = self.iscsi_connection(self.vol, self.location, - self.iqn, False, transport) - if transport is not None: - self.stubs.Set(libvirt_driver, '_get_host_device', - lambda x: self.generate_device(transport, 1, False)) - self.stubs.Set(libvirt_driver, '_validate_transport', - lambda x: True) - libvirt_driver.connect_volume(connection_info, self.disk_info) - libvirt_driver.disconnect_volume(connection_info, "vde") - expected_commands = [('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location), - ('iscsiadm', '-m', 'session'), - ('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location, '--login'), - ('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location, '--op', 'update', - '-n', 'node.startup', '-v', 'automatic'), - ('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location, '--rescan'), - ('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location, '--op', 'update', - '-n', 'node.startup', '-v', 'manual'), - ('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location, '--logout'), - ('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location, '--op', 'delete')] - self.assertEqual(expected_commands, self.executes) - - def test_libvirt_iscsi_driver_with_transport(self): - self.flags(iscsi_iface='fake_transport', group='libvirt') - self.test_libvirt_iscsi_driver('fake_transport') - - def test_libvirt_iscsi_driver_still_in_use(self, transport=None): - # NOTE(vish) exists is to make driver assume connecting worked - self.stubs.Set(os.path, 'exists', lambda x: True) - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - dev_name = self.generate_device(transport, 1, True) - if transport is not None: - self.stubs.Set(libvirt_driver, '_get_host_device', - lambda x: self.generate_device(transport, 1, False)) - self.stubs.Set(libvirt_driver, '_validate_transport', - lambda x: True) - devs = [self.generate_device(transport, 2, False)] - self.stubs.Set(self.fake_conn, '_get_all_block_devices', lambda: devs) - vol = {'id': 1, 'name': self.name} - connection_info = self.iscsi_connection(vol, self.location, self.iqn) - libvirt_driver.connect_volume(connection_info, self.disk_info) - libvirt_driver.disconnect_volume(connection_info, "vde") - expected_commands = [('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location), - ('iscsiadm', '-m', 'session'), - ('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location, '--login'), - ('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location, '--op', 'update', - '-n', 'node.startup', '-v', 'automatic'), - ('iscsiadm', '-m', 'node', '-T', self.iqn, - '-p', self.location, '--rescan'), - ('cp', '/dev/stdin', - '/sys/block/%s/device/delete' % dev_name)] - self.assertEqual(self.executes, expected_commands) - - def test_libvirt_iscsi_driver_still_in_use_with_transport(self): - self.flags(iscsi_iface='fake_transport', group='libvirt') - self.test_libvirt_iscsi_driver_still_in_use('fake_transport') - - def test_libvirt_iscsi_driver_disconnect_multipath_error(self, - transport=None): - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - if transport is None: - prefix = "" - else: - prefix = "pci-0000:00:00.0-" - devs = [self.generate_device(transport, 2, False)] - iscsi_devs = ['%sip-fake-ip-iscsi-fake-portal-lun-2' % prefix] - with contextlib.nested( - mock.patch.object(os.path, 'exists', return_value=True), - mock.patch.object(self.fake_conn, '_get_all_block_devices', - return_value=devs), - mock.patch.object(libvirt_driver, '_rescan_multipath'), - mock.patch.object(libvirt_driver, '_run_multipath'), - mock.patch.object(libvirt_driver, '_get_multipath_device_name', - return_value='/dev/mapper/fake-multipath-devname'), - mock.patch.object(libvirt_driver, '_get_iscsi_devices', - return_value=iscsi_devs), - mock.patch.object(libvirt_driver, - '_get_target_portals_from_iscsiadm_output', - return_value=[('fake-ip', 'fake-portal')]), - mock.patch.object(libvirt_driver, '_get_multipath_iqn', - return_value='fake-portal'), - ) as (mock_exists, mock_devices, mock_rescan_multipath, - mock_run_multipath, mock_device_name, mock_iscsi_devices, - mock_get_portals, mock_get_iqn): - mock_run_multipath.side_effect = processutils.ProcessExecutionError - vol = {'id': 1, 'name': self.name} - connection_info = self.iscsi_connection(vol, self.location, - self.iqn) - libvirt_driver.connect_volume(connection_info, self.disk_info) - - libvirt_driver.use_multipath = True - libvirt_driver.disconnect_volume(connection_info, "vde") - mock_run_multipath.assert_called_once_with( - ['-f', 'fake-multipath-devname'], - check_exit_code=[0, 1]) - - def test_libvirt_iscsi_driver_get_config(self, transport=None): - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - dev_name = self.generate_device(transport, 1, True) - dev_path = '/dev/disk/by-path/%s' % (dev_name) - vol = {'id': 1, 'name': self.name} - connection_info = self.iscsi_connection(vol, self.location, - self.iqn, False, transport) - conf = libvirt_driver.get_config(connection_info, self.disk_info) - tree = conf.format_dom() - self.assertEqual('block', tree.get('type')) - self.assertEqual(dev_path, tree.find('./source').get('dev')) - - libvirt_driver.use_multipath = True - conf = libvirt_driver.get_config(connection_info, self.disk_info) - tree = conf.format_dom() - self.assertEqual('block', tree.get('type')) - self.assertEqual(dev_path, tree.find('./source').get('dev')) - - def test_libvirt_iscsi_driver_get_config_with_transport(self): - self.flags(iscsi_iface = 'fake_transport', group='libvirt') - self.test_libvirt_iscsi_driver_get_config('fake_transport') - - def test_libvirt_iscsi_driver_multipath_id(self): - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - libvirt_driver.use_multipath = True - self.stubs.Set(libvirt_driver, '_run_iscsiadm_bare', - lambda x, check_exit_code: ('',)) - self.stubs.Set(libvirt_driver, '_rescan_iscsi', lambda: None) - self.stubs.Set(libvirt_driver, '_get_host_device', lambda x: None) - self.stubs.Set(libvirt_driver, '_rescan_multipath', lambda: None) - fake_multipath_id = 'fake_multipath_id' - fake_multipath_device = '/dev/mapper/%s' % fake_multipath_id - self.stubs.Set(libvirt_driver, '_get_multipath_device_name', - lambda x: fake_multipath_device) - - def fake_disconnect_volume_multipath_iscsi(iscsi_properties, - multipath_device): - if fake_multipath_device != multipath_device: - raise Exception('Invalid multipath_device.') - - self.stubs.Set(libvirt_driver, '_disconnect_volume_multipath_iscsi', - fake_disconnect_volume_multipath_iscsi) - with mock.patch.object(os.path, 'exists', return_value=True): - vol = {'id': 1, 'name': self.name} - connection_info = self.iscsi_connection(vol, self.location, - self.iqn) - libvirt_driver.connect_volume(connection_info, - self.disk_info) - self.assertEqual(fake_multipath_id, - connection_info['data']['multipath_id']) - libvirt_driver.disconnect_volume(connection_info, "fake") + self.assertIsInstance(libvirt_driver.connector, + connector.ISCSIConnector) def test_sanitize_log_run_iscsiadm(self): - # Tests that the parameters to the _run_iscsiadm function are sanitized - # for passwords when logged. + # Tests that the parameters to the os-brick connector's + # _run_iscsiadm function are sanitized for passwords when logged. def fake_debug(*args, **kwargs): self.assertIn('node.session.auth.password', args[0]) self.assertNotIn('scrubme', args[0]) + def fake_execute(*args, **kwargs): + return (None, None) + libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) + libvirt_driver.connector.set_execute(fake_execute) connection_info = self.iscsi_connection(self.vol, self.location, self.iqn) iscsi_properties = connection_info['data'] - with mock.patch.object(volume.LOG, 'debug', + with mock.patch.object(connector.LOG, 'debug', side_effect=fake_debug) as debug_mock: - libvirt_driver._iscsiadm_update(iscsi_properties, - 'node.session.auth.password', - 'scrubme') + libvirt_driver.connector._iscsiadm_update( + iscsi_properties, 'node.session.auth.password', 'scrubme') + # we don't care what the log message is, we just want to make sure # our stub method is called which asserts the password is scrubbed self.assertTrue(debug_mock.called) @@ -733,337 +533,6 @@ Setting up iSCSI targets: unused self.assertEqual(tree.find('./auth/secret').get('uuid'), SECRET_UUID) libvirt_driver.disconnect_volume(connection_info, 'vde') - def test_libvirt_iscsi_driver_discovery_chap_enable(self): - # NOTE(vish) exists is to make driver assume connecting worked - self.stubs.Set(os.path, 'exists', lambda x: True) - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - libvirt_driver.use_multipath = True - connection_info = self.iscsi_connection_discovery_chap_enable( - self.vol, self.location, - self.iqn) - mpdev_filepath = '/dev/mapper/foo' - libvirt_driver._get_multipath_device_name = lambda x: mpdev_filepath - libvirt_driver.connect_volume(connection_info, self.disk_info) - libvirt_driver.disconnect_volume(connection_info, "vde") - expected_commands = [('iscsiadm', '-m', 'discoverydb', - '-t', 'sendtargets', - '-p', self.location, '--op', 'update', - '-n', 'discovery.sendtargets.auth.authmethod', - '-v', 'CHAP', - '-n', 'discovery.sendtargets.auth.username', - '-v', 'testuser', - '-n', 'discovery.sendtargets.auth.password', - '-v', '123456'), - ('iscsiadm', '-m', 'discoverydb', - '-t', 'sendtargets', - '-p', self.location, '--discover'), - ('iscsiadm', '-m', 'node', '--rescan'), - ('iscsiadm', '-m', 'session', '--rescan'), - ('multipath', '-r'), - ('multipath', '-r'), - ('iscsiadm', '-m', 'discoverydb', - '-t', 'sendtargets', - '-p', self.location, '--op', 'update', - '-n', 'discovery.sendtargets.auth.authmethod', - '-v', 'CHAP', - '-n', 'discovery.sendtargets.auth.username', - '-v', 'testuser', - '-n', 'discovery.sendtargets.auth.password', - '-v', '123456'), - ('iscsiadm', '-m', 'discoverydb', - '-t', 'sendtargets', - '-p', self.location, '--discover'), - ('multipath', '-r')] - self.assertEqual(self.executes, expected_commands) - - def test_libvirt_kvm_volume(self): - self.stubs.Set(os.path, 'exists', lambda x: True) - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - connection_info = self.iscsi_connection(self.vol, self.location, - self.iqn) - conf = libvirt_driver.get_config(connection_info, self.disk_info) - tree = conf.format_dom() - dev_str = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (self.location, - self.iqn) - self.assertEqual(tree.get('type'), 'block') - self.assertEqual(tree.find('./source').get('dev'), dev_str) - libvirt_driver.disconnect_volume(connection_info, 'vde') - - def test_libvirt_kvm_volume_with_multipath(self): - self.flags(iscsi_use_multipath=True, group='libvirt') - self.stubs.Set(os.path, 'exists', lambda x: True) - devs = ['/dev/mapper/sda', '/dev/mapper/sdb'] - self.stubs.Set(self.fake_conn, '_get_all_block_devices', lambda: devs) - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - connection_info = self.iscsi_connection(self.vol, self.location, - self.iqn) - mpdev_filepath = '/dev/mapper/foo' - connection_info['data']['device_path'] = mpdev_filepath - libvirt_driver._get_multipath_device_name = lambda x: mpdev_filepath - iscsi_devs = ['ip-%s-iscsi-%s-lun-0' % (self.location, self.iqn)] - self.stubs.Set(libvirt_driver, '_get_iscsi_devices', - lambda: iscsi_devs) - self.stubs.Set(libvirt_driver, - '_get_target_portals_from_iscsiadm_output', - lambda x: [[self.location, self.iqn]]) - libvirt_driver.connect_volume(connection_info, self.disk_info) - conf = libvirt_driver.get_config(connection_info, self.disk_info) - tree = conf.format_dom() - self.assertEqual(tree.find('./source').get('dev'), mpdev_filepath) - libvirt_driver._get_multipath_iqn = lambda x: self.iqn - libvirt_driver.disconnect_volume(connection_info, 'vde') - expected_multipath_cmd = ('multipath', '-f', 'foo') - self.assertIn(expected_multipath_cmd, self.executes) - - def test_libvirt_kvm_volume_with_multipath_connecting(self): - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - ip_iqns = [[self.location, self.iqn], - ['10.0.2.16:3260', self.iqn], - [self.location, - 'iqn.2010-10.org.openstack:volume-00000002']] - - with contextlib.nested( - mock.patch.object(os.path, 'exists', return_value=True), - mock.patch.object(libvirt_driver, '_run_iscsiadm_bare'), - mock.patch.object(libvirt_driver, - '_get_target_portals_from_iscsiadm_output', - return_value=ip_iqns), - mock.patch.object(libvirt_driver, '_connect_to_iscsi_portal'), - mock.patch.object(libvirt_driver, '_rescan_iscsi'), - mock.patch.object(libvirt_driver, '_get_host_device', - return_value='fake-device'), - mock.patch.object(libvirt_driver, '_rescan_multipath'), - mock.patch.object(libvirt_driver, '_get_multipath_device_name', - return_value='/dev/mapper/fake-mpath-devname') - ) as (mock_exists, mock_run_iscsiadm_bare, mock_get_portals, - mock_connect_iscsi, mock_rescan_iscsi, mock_host_device, - mock_rescan_multipath, mock_device_name): - vol = {'id': 1, 'name': self.name} - connection_info = self.iscsi_connection(vol, self.location, - self.iqn) - libvirt_driver.use_multipath = True - libvirt_driver.connect_volume(connection_info, self.disk_info) - - # Verify that the supplied iqn is used when it shares the same - # iqn between multiple portals. - connection_info = self.iscsi_connection(vol, self.location, - self.iqn) - props1 = connection_info['data'].copy() - props2 = connection_info['data'].copy() - props2['target_portal'] = '10.0.2.16:3260' - expected_calls = [mock.call(props1), mock.call(props2), - mock.call(props1)] - self.assertEqual(expected_calls, mock_connect_iscsi.call_args_list) - - def test_libvirt_kvm_volume_with_multipath_still_in_use(self): - name = 'volume-00000001' - location = '10.0.2.15:3260' - iqn = 'iqn.2010-10.org.openstack:%s' % name - mpdev_filepath = '/dev/mapper/foo' - - def _get_multipath_device_name(path): - if '%s-lun-1' % iqn in path: - return mpdev_filepath - return '/dev/mapper/donotdisconnect' - - self.flags(iscsi_use_multipath=True, group='libvirt') - self.stubs.Set(os.path, 'exists', lambda x: True) - - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - libvirt_driver._get_multipath_device_name =\ - lambda x: _get_multipath_device_name(x) - - block_devs = ['/dev/disks/by-path/%s-iscsi-%s-lun-2' % (location, iqn)] - self.stubs.Set(self.fake_conn, '_get_all_block_devices', - lambda: block_devs) - - vol = {'id': 1, 'name': name} - connection_info = self.iscsi_connection(vol, location, iqn) - connection_info['data']['device_path'] = mpdev_filepath - - libvirt_driver._get_multipath_iqn = lambda x: iqn - - iscsi_devs = ['1.2.3.4-iscsi-%s-lun-1' % iqn, - '%s-iscsi-%s-lun-1' % (location, iqn), - '%s-iscsi-%s-lun-2' % (location, iqn), - 'pci-0000:00:00.0-ip-%s-iscsi-%s-lun-3' % (location, - iqn)] - libvirt_driver._get_iscsi_devices = lambda: iscsi_devs - - self.stubs.Set(libvirt_driver, - '_get_target_portals_from_iscsiadm_output', - lambda x: [[location, iqn]]) - - # Set up disconnect volume mock expectations - self.mox.StubOutWithMock(libvirt_driver, '_delete_device') - self.mox.StubOutWithMock(libvirt_driver, '_rescan_multipath') - libvirt_driver._rescan_multipath() - libvirt_driver._delete_device('/dev/disk/by-path/%s' % iscsi_devs[0]) - libvirt_driver._delete_device('/dev/disk/by-path/%s' % iscsi_devs[1]) - libvirt_driver._rescan_multipath() - - # Ensure that the mpath devices are deleted - self.mox.ReplayAll() - libvirt_driver.disconnect_volume(connection_info, 'vde') - - def test_libvirt_kvm_volume_with_multipath_disconnected(self): - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - volumes = [{'name': self.name, - 'location': self.location, - 'iqn': self.iqn, - 'mpdev_filepath': '/dev/mapper/disconnect'}, - {'name': 'volume-00000002', - 'location': '10.0.2.15:3260', - 'iqn': 'iqn.2010-10.org.openstack:volume-00000002', - 'mpdev_filepath': '/dev/mapper/donotdisconnect'}] - iscsi_devs = ['ip-%s-iscsi-%s-lun-1' % (volumes[0]['location'], - volumes[0]['iqn']), - 'ip-%s-iscsi-%s-lun-1' % (volumes[1]['location'], - volumes[1]['iqn'])] - - def _get_multipath_device_name(path): - if '%s-lun-1' % volumes[0]['iqn'] in path: - return volumes[0]['mpdev_filepath'] - else: - return volumes[1]['mpdev_filepath'] - - def _get_multipath_iqn(mpdev): - if volumes[0]['mpdev_filepath'] == mpdev: - return volumes[0]['iqn'] - else: - return volumes[1]['iqn'] - - with contextlib.nested( - mock.patch.object(os.path, 'exists', return_value=True), - mock.patch.object(self.fake_conn, '_get_all_block_devices', - retrun_value=[volumes[1]['mpdev_filepath']]), - mock.patch.object(libvirt_driver, '_get_multipath_device_name', - _get_multipath_device_name), - mock.patch.object(libvirt_driver, '_get_multipath_iqn', - _get_multipath_iqn), - mock.patch.object(libvirt_driver, '_get_iscsi_devices', - return_value=iscsi_devs), - mock.patch.object(libvirt_driver, - '_get_target_portals_from_iscsiadm_output', - return_value=[[volumes[0]['location'], - volumes[0]['iqn']], - [volumes[1]['location'], - volumes[1]['iqn']]]), - mock.patch.object(libvirt_driver, '_disconnect_mpath') - ) as (mock_exists, mock_devices, mock_device_name, mock_get_iqn, - mock_iscsi_devices, mock_get_portals, mock_disconnect_mpath): - vol = {'id': 1, 'name': volumes[0]['name']} - connection_info = self.iscsi_connection(vol, - volumes[0]['location'], - volumes[0]['iqn']) - connection_info['data']['device_path'] =\ - volumes[0]['mpdev_filepath'] - libvirt_driver.use_multipath = True - libvirt_driver.disconnect_volume(connection_info, 'vde') - # Ensure that the mpath device is disconnected. - ips_iqns = [] - ips_iqns.append([volumes[0]['location'], volumes[0]['iqn']]) - mock_disconnect_mpath.assert_called_once_with( - connection_info['data'], ips_iqns) - - def test_libvirt_kvm_volume_with_multipath_getmpdev(self): - self.flags(iscsi_use_multipath=True, group='libvirt') - self.stubs.Set(os.path, 'exists', lambda x: True) - libvirt_driver = volume.LibvirtISCSIVolumeDriver(self.fake_conn) - name0 = 'volume-00000000' - iqn0 = 'iqn.2010-10.org.openstack:%s' % name0 - dev0 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-0' % (self.location, iqn0) - dev = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (self.location, - self.iqn) - devs = [dev0, dev] - self.stubs.Set(self.fake_conn, '_get_all_block_devices', lambda: devs) - connection_info = self.iscsi_connection(self.vol, self.location, - self.iqn) - mpdev_filepath = '/dev/mapper/foo' - libvirt_driver._get_multipath_device_name = lambda x: mpdev_filepath - self.stubs.Set(libvirt_driver, - '_get_target_portals_from_iscsiadm_output', - lambda x: [['fake_portal1', 'fake_iqn1']]) - libvirt_driver.connect_volume(connection_info, self.disk_info) - conf = libvirt_driver.get_config(connection_info, self.disk_info) - tree = conf.format_dom() - self.assertEqual(tree.find('./source').get('dev'), mpdev_filepath) - libvirt_driver.disconnect_volume(connection_info, 'vde') - - def test_libvirt_kvm_iser_volume_with_multipath(self): - self.flags(iser_use_multipath=True, group='libvirt') - self.stubs.Set(os.path, 'exists', lambda x: True) - self.stubs.Set(time, 'sleep', lambda x: eventlet.sleep(0.1)) - devs = ['/dev/mapper/sda', '/dev/mapper/sdb'] - self.stubs.Set(self.fake_conn, '_get_all_block_devices', lambda: devs) - libvirt_driver = volume.LibvirtISERVolumeDriver(self.fake_conn) - name = 'volume-00000001' - location = '10.0.2.15:3260' - iqn = 'iqn.2010-10.org.iser.openstack:%s' % name - vol = {'id': 1, 'name': name} - connection_info = self.iser_connection(vol, location, iqn) - mpdev_filepath = '/dev/mapper/foo' - connection_info['data']['device_path'] = mpdev_filepath - disk_info = { - "bus": "virtio", - "dev": "vde", - "type": "disk", - } - libvirt_driver._get_multipath_device_name = lambda x: mpdev_filepath - iscsi_devs = ['ip-%s-iscsi-%s-lun-0' % (location, iqn)] - self.stubs.Set(libvirt_driver, '_get_iscsi_devices', - lambda: iscsi_devs) - self.stubs.Set(libvirt_driver, - '_get_target_portals_from_iscsiadm_output', - lambda x: [[location, iqn]]) - self.stubs.Set(libvirt_driver, '_get_host_device', - lambda x: self.generate_device('iser', 0, False)) - - libvirt_driver.connect_volume(connection_info, disk_info) - conf = libvirt_driver.get_config(connection_info, disk_info) - tree = conf.format_dom() - self.assertEqual(tree.find('./source').get('dev'), mpdev_filepath) - libvirt_driver._get_multipath_iqn = lambda x: iqn - libvirt_driver.disconnect_volume(connection_info, 'vde') - expected_multipath_cmd = ('multipath', '-f', 'foo') - self.assertIn(expected_multipath_cmd, self.executes) - - def test_libvirt_kvm_iser_volume_with_multipath_getmpdev(self): - self.flags(iser_use_multipath=True, group='libvirt') - self.stubs.Set(os.path, 'exists', lambda x: True) - self.stubs.Set(time, 'sleep', lambda x: eventlet.sleep(0.1)) - libvirt_driver = volume.LibvirtISERVolumeDriver(self.fake_conn) - name0 = 'volume-00000000' - location0 = '10.0.2.15:3260' - iqn0 = 'iqn.2010-10.org.iser.openstack:%s' % name0 - dev0 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-0' % (location0, iqn0) - name = 'volume-00000001' - location = '10.0.2.15:3260' - iqn = 'iqn.2010-10.org.iser.openstack:%s' % name - vol = {'id': 1, 'name': name} - dev = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location, iqn) - devs = [dev0, dev] - self.stubs.Set(self.fake_conn, '_get_all_block_devices', lambda: devs) - self.stubs.Set(libvirt_driver, '_get_iscsi_devices', lambda: []) - self.stubs.Set(libvirt_driver, '_get_host_device', - lambda x: self.generate_device('iser', 1, False)) - connection_info = self.iser_connection(vol, location, iqn) - mpdev_filepath = '/dev/mapper/foo' - disk_info = { - "bus": "virtio", - "dev": "vde", - "type": "disk", - } - libvirt_driver._get_multipath_device_name = lambda x: mpdev_filepath - self.stubs.Set(libvirt_driver, - '_get_target_portals_from_iscsiadm_output', - lambda x: [['fake_portal1', 'fake_iqn1']]) - libvirt_driver.connect_volume(connection_info, disk_info) - conf = libvirt_driver.get_config(connection_info, disk_info) - tree = conf.format_dom() - self.assertEqual(tree.find('./source').get('dev'), mpdev_filepath) - libvirt_driver.disconnect_volume(connection_info, 'vde') - def test_libvirt_nfs_driver(self): # NOTE(vish) exists is to make driver assume connecting worked mnt_base = '/mnt' @@ -1176,44 +645,11 @@ Setting up iSCSI targets: unused ] self.assertEqual(expected_commands, self.executes) - def aoe_connection(self, shelf, lun): - aoedev = 'e%s.%s' % (shelf, lun) - aoedevpath = '/dev/etherd/%s' % (aoedev) - return { - 'driver_volume_type': 'aoe', - 'data': { - 'target_shelf': shelf, - 'target_lun': lun, - 'device_path': aoedevpath - } - } - @mock.patch('os.path.exists', return_value=True) def test_libvirt_aoe_driver(self, exists): libvirt_driver = volume.LibvirtAOEVolumeDriver(self.fake_conn) - shelf = '100' - lun = '1' - connection_info = self.aoe_connection(shelf, lun) - aoedev = 'e%s.%s' % (shelf, lun) - aoedevpath = '/dev/etherd/%s' % (aoedev) - libvirt_driver.connect_volume(connection_info, self.disk_info) - exists.assert_called_with(aoedevpath) - libvirt_driver.disconnect_volume(connection_info, "vde") - self.assertEqual(aoedevpath, connection_info['data']['device_path']) - expected_commands = [('aoe-revalidate', aoedev)] - self.assertEqual(expected_commands, self.executes) - - def test_libvirt_aoe_driver_get_config(self): - libvirt_driver = volume.LibvirtAOEVolumeDriver(self.fake_conn) - shelf = '100' - lun = '1' - connection_info = self.aoe_connection(shelf, lun) - conf = libvirt_driver.get_config(connection_info, self.disk_info) - tree = conf.format_dom() - aoedevpath = '/dev/etherd/e%s.%s' % (shelf, lun) - self.assertEqual('block', tree.get('type')) - self.assertEqual(aoedevpath, tree.find('./source').get('dev')) - libvirt_driver.disconnect_volume(connection_info, "vde") + self.assertIsInstance(libvirt_driver.connector, + connector.AoEConnector) def test_libvirt_glusterfs_driver(self): mnt_base = '/mnt' @@ -1343,164 +779,15 @@ Setting up iSCSI targets: unused libvirt_driver.disconnect_volume(connection_info, "vde") - def fibrechan_connection(self, volume, location, wwn): - return { - 'driver_volume_type': 'fibrechan', - 'data': { - 'volume_id': volume['id'], - 'target_portal': location, - 'target_wwn': wwn, - 'target_lun': 1, - } - } - def test_libvirt_fibrechan_driver(self): - self.stubs.Set(libvirt_utils, 'get_fc_hbas', - fake_libvirt_utils.get_fc_hbas) - self.stubs.Set(libvirt_utils, 'get_fc_hbas_info', - fake_libvirt_utils.get_fc_hbas_info) - # NOTE(vish) exists is to make driver assume connecting worked - self.stubs.Set(os.path, 'exists', lambda x: True) - self.stubs.Set(os.path, 'realpath', lambda x: '/dev/sdb') libvirt_driver = volume.LibvirtFibreChannelVolumeDriver(self.fake_conn) - multipath_devname = '/dev/md-1' - devices = {"device": multipath_devname, - "id": "1234567890", - "devices": [{'device': '/dev/sdb', - 'address': '1:0:0:1', - 'host': 1, 'channel': 0, - 'id': 0, 'lun': 1}]} - self.stubs.Set(linuxscsi, 'find_multipath_device', lambda x: devices) - self.stubs.Set(linuxscsi, 'remove_device', lambda x: None) - # Should work for string, unicode, and list - wwns = ['1234567890123456', six.text_type('1234567890123456'), - ['1234567890123456', '1234567890123457']] - for wwn in wwns: - connection_info = self.fibrechan_connection(self.vol, - self.location, wwn) - mount_device = "vde" - libvirt_driver.connect_volume(connection_info, self.disk_info) + self.assertIsInstance(libvirt_driver.connector, + connector.FibreChannelConnector) - # Test the scenario where multipath_id is returned - libvirt_driver.disconnect_volume(connection_info, mount_device) - self.assertEqual(multipath_devname, - connection_info['data']['device_path']) - expected_commands = [] - self.assertEqual(expected_commands, self.executes) - # Test the scenario where multipath_id is not returned - connection_info["data"]["devices"] = devices["devices"] - del connection_info["data"]["multipath_id"] - libvirt_driver.disconnect_volume(connection_info, mount_device) - expected_commands = [] - self.assertEqual(expected_commands, self.executes) - - # Should not work for anything other than string, unicode, and list - connection_info = self.fibrechan_connection(self.vol, - self.location, 123) - self.assertRaises(exception.NovaException, - libvirt_driver.connect_volume, - connection_info, self.disk_info) - - self.stubs.Set(libvirt_utils, 'get_fc_hbas', lambda: []) - self.stubs.Set(libvirt_utils, 'get_fc_hbas_info', lambda: []) - self.assertRaises(exception.NovaException, - libvirt_driver.connect_volume, - connection_info, self.disk_info) - - @mock.patch.object(libvirt_utils, 'get_fc_hbas', - side_effect=fake_libvirt_utils.get_fc_hbas) - @mock.patch.object(libvirt_utils, 'get_fc_hbas_info', - side_effect=fake_libvirt_utils.get_fc_hbas_info) - # NOTE(vish) exists is to make driver assume connecting worked - @mock.patch.object(os.path, 'exists', return_value=True) - @mock.patch.object(os.path, 'realpath', return_value='/dev/sdb') - @mock.patch.object(linuxscsi, 'remove_device', return_value=None) - @mock.patch.object(linuxscsi, 'find_multipath_device') - def test_libvirt_fc_find_multipath_device_none(self, mock_find, - mock_remove, mock_realpath, mock_exists, mock_hbasinfo, mock_hbas): + def _test_libvirt_fibrechan_driver_s390(self): libvirt_driver = volume.LibvirtFibreChannelVolumeDriver(self.fake_conn) - multipath_devname = '/dev/md-1' - devices = {"device": multipath_devname, - "id": "1234567890", - "devices": [{'device': '/dev/sdb', - 'address': '1:0:0:1', - 'host': 1, 'channel': 0, - 'id': 0, 'lun': 1}]} - connection_info = self.fibrechan_connection(self.vol, - self.location, - '1234567890123456') - # Test the scenario where multipath_id is returned during - # connect volume - mock_find.return_value = devices - libvirt_driver.connect_volume(connection_info, self.disk_info) - self.assertEqual(0, mock_remove.call_count) - - self.assertIn('multipath_id', connection_info['data']) - - # But NOT during disconnect_volume - mock_find.return_value = None - libvirt_driver.disconnect_volume(connection_info, "vde") - self.assertEqual(0, mock_remove.call_count) - - @mock.patch.object(volume.LibvirtFibreChannelVolumeDriver, - '_remove_lun_from_s390') - def _test_libvirt_fibrechan_driver_s390(self, mock_remove_lun): - self.stubs.Set(libvirt_utils, 'get_fc_hbas', - fake_libvirt_utils.get_fc_hbas) - self.stubs.Set(libvirt_utils, 'get_fc_hbas_info', - fake_libvirt_utils.get_fc_hbas_info) - # NOTE(vish) exists is to make driver assume connecting worked - self.stubs.Set(os.path, 'exists', lambda x: True) - self.stubs.Set(os.path, 'realpath', lambda x: '/dev/sdb') - libvirt_driver = volume.LibvirtFibreChannelVolumeDriver(self.fake_conn) - multipath_devname = '/dev/md-1' - devices = {"device": multipath_devname, - "id": "1234567890", - "devices": [{'device': '/dev/sdb', - 'address': '1:0:0:1', - 'host': 1, 'channel': 0, - 'id': 0, 'lun': 1}]} - self.stubs.Set(linuxscsi, 'find_multipath_device', lambda x: devices) - self.stubs.Set(linuxscsi, 'remove_device', lambda x: None) - # Should work for string, unicode, and list - wwns = ['1234567890123456', six.text_type('1234567890123456'), - ['1234567890123456']] - expected_remove_calls = [] - for wwn in wwns: - self.executes = [] - connection_info = self.fibrechan_connection(self.vol, - self.location, wwn) - expected_remove_calls.append(mock.call(connection_info)) - mount_device = "vde" - libvirt_driver.connect_volume(connection_info, self.disk_info) - - # Test the scenario where multipath_id is returned - libvirt_driver.disconnect_volume(connection_info, mount_device) - self.assertEqual(multipath_devname, - connection_info['data']['device_path']) - expected_commands = [('tee', '-a', - '/sys/bus/ccw/drivers/zfcp/0000:05:00.2/' - '0x1234567890123456/unit_add')] - self.assertEqual(expected_commands, self.executes) - # Test the scenario where multipath_id is not returned - connection_info["data"]["devices"] = devices["devices"] - del connection_info["data"]["multipath_id"] - libvirt_driver.disconnect_volume(connection_info, mount_device) - - mock_remove_lun.assert_has_calls(expected_remove_calls) - - # Should not work for anything other than string, unicode, and list - connection_info = self.fibrechan_connection(self.vol, - self.location, 123) - self.assertRaises(exception.NovaException, - libvirt_driver.connect_volume, - connection_info, self.disk_info) - - self.stubs.Set(libvirt_utils, 'get_fc_hbas', lambda: []) - self.stubs.Set(libvirt_utils, 'get_fc_hbas_info', lambda: []) - self.assertRaises(exception.NovaException, - libvirt_driver.connect_volume, - connection_info, self.disk_info) + self.assertIsInstance(libvirt_driver.connector, + connector.FibreChannelConnectorS390X) @mock.patch.object(platform, 'machine', return_value=arch.S390) def test_libvirt_fibrechan_driver_s390(self, mock_machine): @@ -1510,65 +797,6 @@ Setting up iSCSI targets: unused def test_libvirt_fibrechan_driver_s390x(self, mock_machine): self._test_libvirt_fibrechan_driver_s390() - def test_libvirt_fibrechan_driver_get_config(self): - libvirt_driver = volume.LibvirtFibreChannelVolumeDriver(self.fake_conn) - connection_info = self.fibrechan_connection(self.vol, - self.location, 123) - connection_info['data']['device_path'] = ("/sys/devices/pci0000:00" - "/0000:00:03.0/0000:05:00.3/host2/fc_host/host2") - conf = libvirt_driver.get_config(connection_info, self.disk_info) - tree = conf.format_dom() - self.assertEqual('block', tree.get('type')) - self.assertEqual(connection_info['data']['device_path'], - tree.find('./source').get('dev')) - - def test_libvirt_fibrechan_getpci_num(self): - libvirt_driver = volume.LibvirtFibreChannelVolumeDriver(self.fake_conn) - hba = {'device_path': "/sys/devices/pci0000:00/0000:00:03.0" - "/0000:05:00.3/host2/fc_host/host2"} - pci_num = libvirt_driver._get_pci_num(hba) - self.assertEqual("0000:05:00.3", pci_num) - - hba = {'device_path': "/sys/devices/pci0000:00/0000:00:03.0" - "/0000:05:00.3/0000:06:00.6/host2/fc_host/host2"} - pci_num = libvirt_driver._get_pci_num(hba) - self.assertEqual("0000:06:00.6", pci_num) - - def test_libvirt_fibrechan_get_device_file_path_s390(self): - libvirt_driver = volume.LibvirtFibreChannelVolumeDriver(self.fake_conn) - pci_num = "2310" - wwn = '1234567890123456' - lun = 1 - file_path = libvirt_driver._get_device_file_path_s390(pci_num, - wwn, lun) - expected_path = ("/dev/disk/by-path/ccw-2310-zfcp-" - "1234567890123456:1") - self.assertEqual(expected_path, file_path) - - @mock.patch.object(libvirt_utils, 'get_fc_hbas_info', - fake_libvirt_utils.get_fc_hbas_info) - @mock.patch.object(libvirt_utils, 'perform_unit_remove_for_s390') - def test_libvirt_fibrechan_remove_lun_from_s390(self, mock_unit_remove): - libvirt_driver = volume.LibvirtFibreChannelVolumeDriver(self.fake_conn) - connection_info = { - 'driver_volume_type': 'fibrechan', - 'data': { - 'target_wwn': ['50014380242b9751', - '50014380242b9752'], - 'target_lun': 1, - }} - libvirt_driver._remove_lun_from_s390(connection_info) - - expected_calls = [] - for target_wwn in connection_info['data']['target_wwn']: - # NOTE(mriedem): The device_num value comes from ClassDevicePath - # in fake_libvirt_utils.fake_libvirt_utils. - expected_calls.append(mock.call('0000:05:00.2', - '0x' + target_wwn, - '0x0001000000000000')) - - mock_unit_remove.assert_has_calls(expected_calls) - def test_libvirt_scality_driver(self): tempdir = self.useFixture(fixtures.TempDir()).path TEST_MOUNT = os.path.join(tempdir, 'fake_mount') diff --git a/nova/tests/unit/virt/test_virt_drivers.py b/nova/tests/unit/virt/test_virt_drivers.py index 96be16dfbb..d3f426f710 100644 --- a/nova/tests/unit/virt/test_virt_drivers.py +++ b/nova/tests/unit/virt/test_virt_drivers.py @@ -84,6 +84,9 @@ class _FakeDriverBackendTestCase(object): fake_libvirt_utils import nova.tests.unit.virt.libvirt.fakelibvirt as fakelibvirt + import nova.tests.unit.virt.libvirt.fake_os_brick_connector as \ + fake_os_brick_connector + sys.modules['libvirt'] = fakelibvirt import nova.virt.libvirt.driver import nova.virt.libvirt.firewall @@ -108,6 +111,10 @@ class _FakeDriverBackendTestCase(object): 'nova.virt.libvirt.firewall.libvirt', fakelibvirt)) + self.useFixture(fixtures.MonkeyPatch( + 'nova.virt.libvirt.driver.connector', + fake_os_brick_connector)) + fakelibvirt.disable_event_thread(self) self.flags(rescue_image_id="2", @@ -459,7 +466,7 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase): '/dev/sda')) self.assertIsNone( self.connection.detach_volume(connection_info, instance_ref, - '/dev/sda')) + '/dev/sda')) @catch_notimplementederror def test_swap_volume(self): diff --git a/nova/tests/unit/virt/test_volumeutils.py b/nova/tests/unit/virt/test_volumeutils.py index 1cf9537482..5d59dd910d 100644 --- a/nova/tests/unit/virt/test_volumeutils.py +++ b/nova/tests/unit/virt/test_volumeutils.py @@ -14,34 +14,27 @@ # License for the specific language governing permissions and limitations # under the License. """ -Tests fot virt volumeutils. +Tests for virt volumeutils. """ +import mock +from os_brick.initiator import connector -from nova import exception from nova import test -from nova import utils from nova.virt import volumeutils class VolumeUtilsTestCase(test.NoDBTestCase): - def test_get_iscsi_initiator(self): - self.mox.StubOutWithMock(utils, 'execute') + + @mock.patch.object(connector.ISCSIConnector, 'get_initiator', + return_value='fake.initiator.iqn') + def test_get_iscsi_initiator(self, fake_initiator): initiator = 'fake.initiator.iqn' - rval = ("junk\nInitiatorName=%s\njunk\n" % initiator, None) - utils.execute('cat', '/etc/iscsi/initiatorname.iscsi', - run_as_root=True).AndReturn(rval) # Start test - self.mox.ReplayAll() result = volumeutils.get_iscsi_initiator() self.assertEqual(initiator, result) - def test_get_missing_iscsi_initiator(self): - self.mox.StubOutWithMock(utils, 'execute') - file_path = '/etc/iscsi/initiatorname.iscsi' - utils.execute('cat', file_path, run_as_root=True).AndRaise( - exception.FileNotFound(file_path=file_path) - ) - # Start test - self.mox.ReplayAll() + @mock.patch.object(connector.ISCSIConnector, 'get_initiator', + return_value=None) + def test_get_missing_iscsi_initiator(self, fake_initiator): result = volumeutils.get_iscsi_initiator() - self.assertEqual('', result) + self.assertIsNone(result) diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index cfef68d3f8..7c6e40e224 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -43,6 +43,7 @@ import eventlet from eventlet import greenthread from eventlet import tpool from lxml import etree +from os_brick.initiator import connector from oslo_concurrency import processutils from oslo_config import cfg from oslo_log import log as logging @@ -250,6 +251,8 @@ CONF.import_opt('proxyclient_address', 'nova.console.serial', CONF.import_opt('hw_disk_discard', 'nova.virt.libvirt.imagebackend', group='libvirt') CONF.import_group('workarounds', 'nova.utils') +CONF.import_opt('iscsi_use_multipath', 'nova.virt.libvirt.volume', + group='libvirt') DEFAULT_FIREWALL_DRIVER = "%s.%s" % ( libvirt_firewall.__name__, @@ -959,37 +962,12 @@ class LibvirtDriver(driver.ComputeDriver): return [] def get_volume_connector(self, instance): - if self._initiator is None: - self._initiator = libvirt_utils.get_iscsi_initiator() - if not self._initiator: - LOG.debug('Could not determine iscsi initiator name', - instance=instance) - - if self._fc_wwnns is None: - self._fc_wwnns = libvirt_utils.get_fc_wwnns() - if not self._fc_wwnns or len(self._fc_wwnns) == 0: - LOG.debug('Could not determine fibre channel ' - 'world wide node names', - instance=instance) - - if self._fc_wwpns is None: - self._fc_wwpns = libvirt_utils.get_fc_wwpns() - if not self._fc_wwpns or len(self._fc_wwpns) == 0: - LOG.debug('Could not determine fibre channel ' - 'world wide port names', - instance=instance) - - connector = {'ip': CONF.my_block_storage_ip, - 'host': CONF.host} - - if self._initiator: - connector['initiator'] = self._initiator - - if self._fc_wwnns and self._fc_wwpns: - connector["wwnns"] = self._fc_wwnns - connector["wwpns"] = self._fc_wwpns - - return connector + root_helper = utils._get_root_helper() + return connector.get_connector_properties( + root_helper, CONF.my_block_storage_ip, + CONF.libvirt.iscsi_use_multipath, + enforce_multipath=True, + host=CONF.host) def _cleanup_resize(self, instance, network_info): # NOTE(wangpan): we get the pre-grizzly instance path firstly, diff --git a/nova/virt/libvirt/utils.py b/nova/virt/libvirt/utils.py index 52b315eee1..db905853d7 100644 --- a/nova/virt/libvirt/utils.py +++ b/nova/virt/libvirt/utils.py @@ -20,7 +20,6 @@ import errno import os -import platform import re from lxml import etree @@ -31,8 +30,6 @@ from oslo_log import log as logging from nova.compute import arch from nova.i18n import _ from nova.i18n import _LI -from nova.i18n import _LW -from nova.storage import linuxscsi from nova import utils from nova.virt import images from nova.virt.libvirt import config as vconfig @@ -59,110 +56,6 @@ def get_iscsi_initiator(): return volumeutils.get_iscsi_initiator() -def get_fc_hbas(): - """Get the Fibre Channel HBA information.""" - out = None - try: - out, err = execute('systool', '-c', 'fc_host', '-v', - run_as_root=True) - except processutils.ProcessExecutionError as exc: - # This handles the case where rootwrap is used - # and systool is not installed - # 96 = nova.cmd.rootwrap.RC_NOEXECFOUND: - if exc.exit_code == 96: - LOG.warn(_LW("systool is not installed")) - return [] - except OSError as exc: - # This handles the case where rootwrap is NOT used - # and systool is not installed - if exc.errno == errno.ENOENT: - LOG.warn(_LW("systool is not installed")) - return [] - - if out is None: - raise RuntimeError(_("Cannot find any Fibre Channel HBAs")) - - lines = out.split('\n') - # ignore the first 2 lines - lines = lines[2:] - hbas = [] - hba = {} - lastline = None - for line in lines: - line = line.strip() - # 2 newlines denotes a new hba port - if line == '' and lastline == '': - if len(hba) > 0: - hbas.append(hba) - hba = {} - else: - val = line.split('=') - if len(val) == 2: - key = val[0].strip().replace(" ", "") - value = val[1].strip() - hba[key] = value.replace('"', '') - lastline = line - - return hbas - - -def get_fc_hbas_info(): - """Get Fibre Channel WWNs and device paths from the system, if any.""" - # Note modern linux kernels contain the FC HBA's in /sys - # and are obtainable via the systool app - hbas = get_fc_hbas() - hbas_info = [] - for hba in hbas: - # Systems implementing the S390 architecture support virtual HBAs - # may be online, or offline. This function should only return - # virtual HBAs in the online state - if (platform.machine() in (arch.S390, arch.S390X) and - hba['port_state'].lower() != 'online'): - continue - - wwpn = hba['port_name'].replace('0x', '') - wwnn = hba['node_name'].replace('0x', '') - device_path = hba['ClassDevicepath'] - device = hba['ClassDevice'] - hbas_info.append({'port_name': wwpn, - 'node_name': wwnn, - 'host_device': device, - 'device_path': device_path}) - return hbas_info - - -def get_fc_wwpns(): - """Get Fibre Channel WWPNs from the system, if any.""" - # Note modern linux kernels contain the FC HBA's in /sys - # and are obtainable via the systool app - hbas = get_fc_hbas() - - wwpns = [] - if hbas: - for hba in hbas: - if hba['port_state'] == 'Online': - wwpn = hba['port_name'].replace('0x', '') - wwpns.append(wwpn) - - return wwpns - - -def get_fc_wwnns(): - """Get Fibre Channel WWNNs from the system, if any.""" - # Note modern linux kernels contain the FC HBA's in /sys - # and are obtainable via the systool app - hbas = get_fc_hbas() - - wwnns = [] - if hbas: - for hba in hbas: - if hba['port_state'] == 'Online': - wwnn = hba['node_name'].replace('0x', '') - wwnns.append(wwnn) - - return wwnns - - def create_image(disk_format, path, size): """Create a disk image @@ -588,51 +481,3 @@ def is_mounted(mount_path, source=None): def is_valid_hostname(hostname): return re.match(r"^[\w\-\.:]+$", hostname) - - -def perform_unit_add_for_s390(device_number, target_wwn, lun): - """Write the LUN to the port's unit_add attribute.""" - # NOTE If LUN scanning is turned off on systems following the s390, - # or s390x architecture LUNs need to be added to the configuration - # using the unit_add call. The unit_add call may fail if a target_wwn - # is not accessible for the HBA specified by the device_number. - # This can be an expected situation in multipath configurations. - # This method will thus only log a warning message in case the - # unit_add call fails. - LOG.debug("perform unit_add for s390: device_number=(%(device_num)s) " - "target_wwn=(%(target_wwn)s) target_lun=(%(target_lun)s)", - {'device_num': device_number, - 'target_wwn': target_wwn, - 'target_lun': lun}) - zfcp_device_command = ("/sys/bus/ccw/drivers/zfcp/%s/%s/unit_add" % - (device_number, target_wwn)) - try: - linuxscsi.echo_scsi_command(zfcp_device_command, lun) - except processutils.ProcessExecutionError as exc: - LOG.warn(_LW("unit_add call failed; exit code (%(code)s), " - "stderr (%(stderr)s)"), - {'code': exc.exit_code, 'stderr': exc.stderr}) - - -def perform_unit_remove_for_s390(device_number, target_wwn, lun): - """Write the LUN to the port's unit_remove attribute.""" - # If LUN scanning is turned off on systems following the s390, or s390x - # architecture LUNs need to be removed from the configuration using the - # unit_remove call. The unit_remove call may fail if the LUN is not - # part of the configuration anymore. This may be an expected situation. - # For exmple, if LUN scanning is turned on. - # This method will thus only log a warning message in case the - # unit_remove call fails. - LOG.debug("perform unit_remove for s390: device_number=(%(device_num)s) " - "target_wwn=(%(target_wwn)s) target_lun=(%(target_lun)s)", - {'device_num': device_number, - 'target_wwn': target_wwn, - 'target_lun': lun}) - zfcp_device_command = ("/sys/bus/ccw/drivers/zfcp/%s/%s/unit_remove" % - (device_number, target_wwn)) - try: - linuxscsi.echo_scsi_command(zfcp_device_command, lun) - except processutils.ProcessExecutionError as exc: - LOG.warn(_LW("unit_remove call failed; exit code (%(code)s), " - "stderr (%(stderr)s)"), - {'code': exc.exit_code, 'stderr': exc.stderr}) diff --git a/nova/virt/libvirt/volume.py b/nova/virt/libvirt/volume.py index 584de1d39b..243a021ab5 100644 --- a/nova/virt/libvirt/volume.py +++ b/nova/virt/libvirt/volume.py @@ -17,29 +17,23 @@ """Volume drivers for libvirt.""" import errno -import glob import os -import platform import re -import time +from os_brick.initiator import connector from oslo_concurrency import processutils from oslo_config import cfg from oslo_log import log as logging -from oslo_service import loopingcall -from oslo_utils import strutils import six from six.moves import urllib import six.moves.urllib.parse as urlparse -from nova.compute import arch from nova import exception from nova.i18n import _ from nova.i18n import _LE from nova.i18n import _LI from nova.i18n import _LW from nova import paths -from nova.storage import linuxscsi from nova import utils from nova.virt.libvirt import config as vconfig from nova.virt.libvirt import quobyte @@ -302,89 +296,26 @@ class LibvirtNetVolumeDriver(LibvirtBaseVolumeDriver): class LibvirtISCSIVolumeDriver(LibvirtBaseVolumeDriver): """Driver to attach Network volumes to libvirt.""" - supported_transports = ['be2iscsi', 'bnx2i', 'cxgb3i', - 'cxgb4i', 'qla4xxx', 'ocs'] def __init__(self, connection): super(LibvirtISCSIVolumeDriver, self).__init__(connection, is_block_dev=True) - self.num_scan_tries = CONF.libvirt.num_iscsi_scan_tries - self.use_multipath = CONF.libvirt.iscsi_use_multipath - if CONF.libvirt.iscsi_iface: - self.transport = CONF.libvirt.iscsi_iface - else: - self.transport = 'default' + + # Call the factory here so we can support + # more than x86 architectures. + self.connector = connector.InitiatorConnector.factory( + 'ISCSI', utils._get_root_helper(), + use_multipath=CONF.libvirt.iscsi_use_multipath, + device_scan_attempts=CONF.libvirt.num_iscsi_scan_tries, + transport=self._get_transport()) def _get_transport(self): - if self._validate_transport(self.transport): - return self.transport + if CONF.libvirt.iscsi_iface: + transport = CONF.libvirt.iscsi_iface else: - return 'default' + transport = 'default' - def _validate_transport(self, transport_iface): - """Check that given iscsi_iface uses only supported transports - - Accepted transport names for provided iface param are - be2iscsi, bnx2i, cxgb3i, cxgb4i, qla4xxx and ocs. iSER uses it's - own separate driver. Note the difference between transport and - iface; unlike iscsi_tcp/iser, this is not one and the same for - offloaded transports, where the default format is - transport_name.hwaddress - """ - # We can support iser here as well, but currently reject it as the - # separate iser driver has not yet been deprecated. - if transport_iface == 'default': - return True - # Will return (6) if iscsi_iface file was not found, or (2) if iscsid - # could not be contacted - out = self._run_iscsiadm_bare(['-m', - 'iface', - '-I', - transport_iface], - check_exit_code=[0, 2, 6])[0] or "" - LOG.debug("iscsiadm %(iface)s configuration: stdout=%(out)s", - {'iface': transport_iface, 'out': out}) - for data in [line.split() for line in out.splitlines()]: - if data[0] == 'iface.transport_name': - if data[2] in self.supported_transports: - return True - - LOG.warn(_LW("No useable transport found for iscsi iface %s. " - "Falling back to default transport"), - transport_iface) - return False - - def _run_iscsiadm(self, iscsi_properties, iscsi_command, **kwargs): - check_exit_code = kwargs.pop('check_exit_code', 0) - (out, err) = utils.execute('iscsiadm', '-m', 'node', '-T', - iscsi_properties['target_iqn'], - '-p', iscsi_properties['target_portal'], - *iscsi_command, run_as_root=True, - check_exit_code=check_exit_code) - msg = ('iscsiadm %(command)s: stdout=%(out)s stderr=%(err)s' % - {'command': iscsi_command, 'out': out, 'err': err}) - # NOTE(bpokorny): iscsi_command can contain passwords so we need to - # sanitize the password in the message. - LOG.debug(strutils.mask_password(msg)) - return (out, err) - - def _iscsiadm_update(self, iscsi_properties, property_key, property_value, - **kwargs): - iscsi_command = ('--op', 'update', '-n', property_key, - '-v', property_value) - return self._run_iscsiadm(iscsi_properties, iscsi_command, **kwargs) - - def _get_target_portals_from_iscsiadm_output(self, output): - # return both portals and iqns - # - # as we are parsing a command line utility, allow for the - # possibility that additional debug data is spewed in the - # stream, and only grab actual ip / iqn lines. - targets = [] - for data in [line.split() for line in output.splitlines()]: - if len(data) == 2 and data[1].startswith('iqn.'): - targets.append(data) - return targets + return transport def get_config(self, connection_info, disk_info): """Returns xml for libvirt.""" @@ -394,486 +325,43 @@ class LibvirtISCSIVolumeDriver(LibvirtBaseVolumeDriver): conf.source_path = connection_info['data']['device_path'] return conf - @utils.synchronized('connect_volume') def connect_volume(self, connection_info, disk_info): """Attach the volume to instance_name.""" - iscsi_properties = connection_info['data'] - # multipath installed, discovering other targets if available - # multipath should be configured on the nova-compute node, - # in order to fit storage vendor - if self.use_multipath: - out = self._run_iscsiadm_discover(iscsi_properties) + LOG.debug("Calling os-brick to attach iSCSI Volume") + device_info = self.connector.connect_volume(connection_info['data']) + LOG.debug("Attached iSCSI volume %s", device_info) - # There are two types of iSCSI multipath devices. One which shares - # the same iqn between multiple portals, and the other which use - # different iqns on different portals. Try to identify the type by - # checking the iscsiadm output if the iqn is used by multiple - # portals. If it is, it's the former, so use the supplied iqn. - # Otherwise, it's the latter, so try the ip,iqn combinations to - # find the targets which constitutes the multipath device. - ips_iqns = self._get_target_portals_from_iscsiadm_output(out) - same_portal = False - all_portals = set() - match_portals = set() - for ip, iqn in ips_iqns: - all_portals.add(ip) - if iqn == iscsi_properties['target_iqn']: - match_portals.add(ip) - if len(all_portals) == len(match_portals): - same_portal = True + connection_info['data']['device_path'] = device_info['path'] - for ip, iqn in ips_iqns: - props = iscsi_properties.copy() - props['target_portal'] = ip.split(",")[0] - if not same_portal: - props['target_iqn'] = iqn - self._connect_to_iscsi_portal(props) - - self._rescan_iscsi() - else: - self._connect_to_iscsi_portal(iscsi_properties) - - # Detect new/resized LUNs for existing sessions - self._run_iscsiadm(iscsi_properties, ("--rescan",)) - - host_device = self._get_host_device(iscsi_properties) - - # The /dev/disk/by-path/... node is not always present immediately - # TODO(justinsb): This retry-with-delay is a pattern, move to utils? - tries = 0 - disk_dev = disk_info['dev'] - - # Check host_device only when transport is used, since otherwise it is - # directly derived from properties. Only needed for unit tests - while ((self._get_transport() != "default" and not host_device) - or not os.path.exists(host_device)): - if tries >= self.num_scan_tries: - raise exception.NovaException(_("iSCSI device not found at %s") - % (host_device)) - - LOG.warn(_LW("ISCSI volume not yet found at: %(disk_dev)s. " - "Will rescan & retry. Try number: %(tries)s"), - {'disk_dev': disk_dev, 'tries': tries}) - - # The rescan isn't documented as being necessary(?), but it helps - self._run_iscsiadm(iscsi_properties, ("--rescan",)) - - # For offloaded open-iscsi transports, host_device cannot be - # guessed unlike iscsi_tcp where it can be obtained from - # properties, so try and get it again. - if not host_device and self._get_transport() != "default": - host_device = self._get_host_device(iscsi_properties) - - tries = tries + 1 - if not host_device or not os.path.exists(host_device): - time.sleep(tries ** 2) - - if tries != 0: - LOG.debug("Found iSCSI node %(disk_dev)s " - "(after %(tries)s rescans)", - {'disk_dev': disk_dev, - 'tries': tries}) - - if self.use_multipath: - # we use the multipath device instead of the single path device - self._rescan_multipath() - - multipath_device = self._get_multipath_device_name(host_device) - - if multipath_device is not None: - host_device = multipath_device - connection_info['data']['multipath_id'] = \ - multipath_device.split('/')[-1] - - connection_info['data']['device_path'] = host_device - - def _run_iscsiadm_discover(self, iscsi_properties): - def run_iscsiadm_update_discoverydb(): - return utils.execute( - 'iscsiadm', - '-m', 'discoverydb', - '-t', 'sendtargets', - '-p', iscsi_properties['target_portal'], - '--op', 'update', - '-n', "discovery.sendtargets.auth.authmethod", - '-v', iscsi_properties['discovery_auth_method'], - '-n', "discovery.sendtargets.auth.username", - '-v', iscsi_properties['discovery_auth_username'], - '-n', "discovery.sendtargets.auth.password", - '-v', iscsi_properties['discovery_auth_password'], - run_as_root=True) - - out = None - if iscsi_properties.get('discovery_auth_method'): - try: - run_iscsiadm_update_discoverydb() - except processutils.ProcessExecutionError as exc: - # iscsiadm returns 6 for "db record not found" - if exc.exit_code == 6: - (out, err) = utils.execute( - 'iscsiadm', - '-m', 'discoverydb', - '-t', 'sendtargets', - '-p', iscsi_properties['target_portal'], - '--op', 'new', - run_as_root=True) - run_iscsiadm_update_discoverydb() - else: - raise - - out = self._run_iscsiadm_bare( - ['-m', - 'discoverydb', - '-t', - 'sendtargets', - '-p', - iscsi_properties['target_portal'], - '--discover'], - check_exit_code=[0, 255])[0] or "" - else: - out = self._run_iscsiadm_bare( - ['-m', - 'discovery', - '-t', - 'sendtargets', - '-p', - iscsi_properties['target_portal']], - check_exit_code=[0, 255])[0] or "" - return out - - @utils.synchronized('connect_volume') def disconnect_volume(self, connection_info, disk_dev): """Detach the volume from instance_name.""" - iscsi_properties = connection_info['data'] - host_device = self._get_host_device(iscsi_properties) - multipath_device = None - if self.use_multipath: - if 'multipath_id' in iscsi_properties: - multipath_device = ('/dev/mapper/%s' % - iscsi_properties['multipath_id']) - else: - multipath_device = self._get_multipath_device_name(host_device) + + LOG.debug("calling os-brick to detach iSCSI Volume") + self.connector.disconnect_volume(connection_info['data'], None) + LOG.debug("Disconnected iSCSI Volume %s", disk_dev) super(LibvirtISCSIVolumeDriver, self).disconnect_volume(connection_info, disk_dev) - if self.use_multipath and multipath_device: - return self._disconnect_volume_multipath_iscsi(iscsi_properties, - multipath_device) - - # NOTE(vish): Only disconnect from the target if no luns from the - # target are in use. - device_byname = ("ip-%s-iscsi-%s-lun-" % - (iscsi_properties['target_portal'], - iscsi_properties['target_iqn'])) - devices = self.connection._get_all_block_devices() - devices = [dev for dev in devices if (device_byname in dev - and - dev.startswith( - '/dev/disk/by-path/'))] - if not devices: - self._disconnect_from_iscsi_portal(iscsi_properties) - elif host_device not in devices: - # Delete device if LUN is not in use by another instance - self._delete_device(host_device) - - def _delete_device(self, device_path): - device_name = os.path.basename(os.path.realpath(device_path)) - delete_control = '/sys/block/' + device_name + '/device/delete' - if os.path.exists(delete_control): - # Copy '1' from stdin to the device delete control file - utils.execute('cp', '/dev/stdin', delete_control, - process_input='1', run_as_root=True) - else: - LOG.warn(_LW("Unable to delete volume device %s"), device_name) - - def _remove_multipath_device_descriptor(self, disk_descriptor): - disk_descriptor = disk_descriptor.replace('/dev/mapper/', '') - try: - self._run_multipath(['-f', disk_descriptor], - check_exit_code=[0, 1]) - except processutils.ProcessExecutionError as exc: - # Because not all cinder drivers need to remove the dev mapper, - # here just logs a warning to avoid affecting those drivers in - # exceptional cases. - LOG.warn(_LW('Failed to remove multipath device descriptor ' - '%(dev_mapper)s. Exception message: %(msg)s') - % {'dev_mapper': disk_descriptor, - 'msg': exc.message}) - - def _disconnect_volume_multipath_iscsi(self, iscsi_properties, - multipath_device): - self._rescan_multipath() - block_devices = self.connection._get_all_block_devices() - devices = [] - for dev in block_devices: - if "/mapper/" in dev: - devices.append(dev) - else: - mpdev = self._get_multipath_device_name(dev) - if mpdev: - devices.append(mpdev) - - # Do a discovery to find all targets. - # Targets for multiple paths for the same multipath device - # may not be the same. - out = self._run_iscsiadm_discover(iscsi_properties) - - # Extract targets for the current multipath device. - ips_iqns = [] - entries = self._get_iscsi_devices() - for ip, iqn in self._get_target_portals_from_iscsiadm_output(out): - ip_iqn = "%s-iscsi-%s" % (ip.split(",")[0], iqn) - for entry in entries: - entry_ip_iqn = entry.split("-lun-")[0] - if entry_ip_iqn[:3] == "ip-": - entry_ip_iqn = entry_ip_iqn[3:] - elif entry_ip_iqn[:4] == "pci-": - # Look at an offset of len('pci-0000:00:00.0') - offset = entry_ip_iqn.find("ip-", 16, 21) - entry_ip_iqn = entry_ip_iqn[(offset + 3):] - if (ip_iqn != entry_ip_iqn): - continue - entry_real_path = os.path.realpath("/dev/disk/by-path/%s" % - entry) - entry_mpdev = self._get_multipath_device_name(entry_real_path) - if entry_mpdev == multipath_device: - ips_iqns.append([ip, iqn]) - break - - if not devices: - # disconnect if no other multipath devices - self._disconnect_mpath(iscsi_properties, ips_iqns) - return - - # Get a target for all other multipath devices - other_iqns = [self._get_multipath_iqn(device) - for device in devices] - # Get all the targets for the current multipath device - current_iqns = [iqn for ip, iqn in ips_iqns] - - in_use = False - for current in current_iqns: - if current in other_iqns: - in_use = True - break - - # If no other multipath device attached has the same iqn - # as the current device - if not in_use: - # disconnect if no other multipath devices with same iqn - self._disconnect_mpath(iscsi_properties, ips_iqns) - return - elif multipath_device not in devices: - # delete the devices associated w/ the unused multipath - self._delete_mpath(iscsi_properties, multipath_device, ips_iqns) - - # else do not disconnect iscsi portals, - # as they are used for other luns, - # just remove multipath mapping device descriptor - self._remove_multipath_device_descriptor(multipath_device) - return - - def _connect_to_iscsi_portal(self, iscsi_properties): - # NOTE(vish): If we are on the same host as nova volume, the - # discovery makes the target so we don't need to - # run --op new. Therefore, we check to see if the - # target exists, and if we get 255 (Not Found), then - # we run --op new. This will also happen if another - # volume is using the same target. - try: - self._run_iscsiadm(iscsi_properties, ()) - except processutils.ProcessExecutionError as exc: - # iscsiadm returns 21 for "No records found" after version 2.0-871 - if exc.exit_code in [21, 255]: - self._reconnect(iscsi_properties) - else: - raise - - if iscsi_properties.get('auth_method'): - self._iscsiadm_update(iscsi_properties, - "node.session.auth.authmethod", - iscsi_properties['auth_method']) - self._iscsiadm_update(iscsi_properties, - "node.session.auth.username", - iscsi_properties['auth_username']) - self._iscsiadm_update(iscsi_properties, - "node.session.auth.password", - iscsi_properties['auth_password']) - - # duplicate logins crash iscsiadm after load, - # so we scan active sessions to see if the node is logged in. - out = self._run_iscsiadm_bare(["-m", "session"], - run_as_root=True, - check_exit_code=[0, 1, 21])[0] or "" - - portals = [{'portal': p.split(" ")[2], 'iqn': p.split(" ")[3]} - for p in out.splitlines() if p.startswith("tcp:")] - - stripped_portal = iscsi_properties['target_portal'].split(",")[0] - if len(portals) == 0 or len([s for s in portals - if stripped_portal == - s['portal'].split(",")[0] - and - s['iqn'] == - iscsi_properties['target_iqn']] - ) == 0: - try: - self._run_iscsiadm(iscsi_properties, - ("--login",), - check_exit_code=[0, 255]) - except processutils.ProcessExecutionError as err: - # as this might be one of many paths, - # only set successful logins to startup automatically - if err.exit_code in [15]: - self._iscsiadm_update(iscsi_properties, - "node.startup", - "automatic") - return - - self._iscsiadm_update(iscsi_properties, - "node.startup", - "automatic") - - def _disconnect_from_iscsi_portal(self, iscsi_properties): - self._iscsiadm_update(iscsi_properties, "node.startup", "manual", - check_exit_code=[0, 21, 255]) - self._run_iscsiadm(iscsi_properties, ("--logout",), - check_exit_code=[0, 21, 255]) - self._run_iscsiadm(iscsi_properties, ('--op', 'delete'), - check_exit_code=[0, 21, 255]) - - def _get_multipath_device_name(self, single_path_device): - device = os.path.realpath(single_path_device) - - out = self._run_multipath(['-ll', - device], - check_exit_code=[0, 1])[0] - mpath_line = [line for line in out.splitlines() - if "scsi_id" not in line] # ignore udev errors - if len(mpath_line) > 0 and len(mpath_line[0]) > 0: - return "/dev/mapper/%s" % mpath_line[0].split(" ")[0] - - return None - - def _get_iscsi_devices(self): - try: - devices = list(os.walk('/dev/disk/by-path'))[0][-1] - except IndexError: - return [] - iscsi_devs = [] - for entry in devices: - if (entry.startswith("ip-") or - (entry.startswith('pci-') and 'ip-' in entry)): - iscsi_devs.append(entry) - - return iscsi_devs - - def _delete_mpath(self, iscsi_properties, multipath_device, ips_iqns): - entries = self._get_iscsi_devices() - # Loop through ips_iqns to construct all paths - iqn_luns = [] - for ip, iqn in ips_iqns: - iqn_lun = '%s-lun-%s' % (iqn, - iscsi_properties.get('target_lun', 0)) - iqn_luns.append(iqn_lun) - for dev in ['/dev/disk/by-path/%s' % dev for dev in entries]: - for iqn_lun in iqn_luns: - if iqn_lun in dev: - self._delete_device(dev) - - self._rescan_multipath() - - def _disconnect_mpath(self, iscsi_properties, ips_iqns): - for ip, iqn in ips_iqns: - props = iscsi_properties.copy() - props['target_portal'] = ip - props['target_iqn'] = iqn - self._disconnect_from_iscsi_portal(props) - - self._rescan_multipath() - - def _get_multipath_iqn(self, multipath_device): - entries = self._get_iscsi_devices() - for entry in entries: - entry_real_path = os.path.realpath("/dev/disk/by-path/%s" % entry) - entry_multipath = self._get_multipath_device_name(entry_real_path) - if entry_multipath == multipath_device: - return entry.split("iscsi-")[1].split("-lun")[0] - return None - - def _run_iscsiadm_bare(self, iscsi_command, **kwargs): - check_exit_code = kwargs.pop('check_exit_code', 0) - (out, err) = utils.execute('iscsiadm', - *iscsi_command, - run_as_root=True, - check_exit_code=check_exit_code) - LOG.debug("iscsiadm %(command)s: stdout=%(out)s stderr=%(err)s", - {'command': iscsi_command, 'out': out, 'err': err}) - return (out, err) - - def _run_multipath(self, multipath_command, **kwargs): - check_exit_code = kwargs.pop('check_exit_code', 0) - (out, err) = utils.execute('multipath', - *multipath_command, - run_as_root=True, - check_exit_code=check_exit_code) - LOG.debug("multipath %(command)s: stdout=%(out)s stderr=%(err)s", - {'command': multipath_command, 'out': out, 'err': err}) - return (out, err) - - def _rescan_iscsi(self): - self._run_iscsiadm_bare(('-m', 'node', '--rescan'), - check_exit_code=[0, 1, 21, 255]) - self._run_iscsiadm_bare(('-m', 'session', '--rescan'), - check_exit_code=[0, 1, 21, 255]) - - def _rescan_multipath(self): - self._run_multipath(['-r'], check_exit_code=[0, 1, 21]) - - def _get_host_device(self, transport_properties): - """Find device path in devtemfs.""" - device = ("ip-%s-iscsi-%s-lun-%s" % - (transport_properties['target_portal'], - transport_properties['target_iqn'], - transport_properties.get('target_lun', 0))) - if self._get_transport() == "default": - return ("/dev/disk/by-path/%s" % device) - else: - host_device = None - look_for_device = glob.glob('/dev/disk/by-path/*%s' % device) - if look_for_device: - host_device = look_for_device[0] - return host_device - - def _reconnect(self, iscsi_properties): - # Note: iscsiadm does not support changing iface.iscsi_ifacename - # via --op update, so we do this at creation time - self._run_iscsiadm(iscsi_properties, - ('--interface', self._get_transport(), - '--op', 'new')) - class LibvirtISERVolumeDriver(LibvirtISCSIVolumeDriver): """Driver to attach Network volumes to libvirt.""" + def __init__(self, connection): super(LibvirtISERVolumeDriver, self).__init__(connection) - self.num_scan_tries = CONF.libvirt.num_iser_scan_tries - self.use_multipath = CONF.libvirt.iser_use_multipath + + # Call the factory here so we can support + # more than x86 architectures. + self.connector = connector.InitiatorConnector.factory( + 'ISER', utils._get_root_helper(), + use_multipath=CONF.libvirt.iser_use_multipath, + device_scan_attempts=CONF.libvirt.num_iser_scan_tries, + transport=self._get_transport()) def _get_transport(self): return 'iser' - def _get_multipath_iqn(self, multipath_device): - entries = self._get_iscsi_devices() - for entry in entries: - entry_real_path = os.path.realpath("/dev/disk/by-path/%s" % entry) - entry_multipath = self._get_multipath_device_name(entry_real_path) - if entry_multipath == multipath_device: - return entry.split("iser-")[1].split("-lun")[0] - return None - class LibvirtNFSVolumeDriver(LibvirtBaseVolumeDriver): """Class implements libvirt part of volume driver for NFS.""" @@ -1024,24 +512,11 @@ class LibvirtAOEVolumeDriver(LibvirtBaseVolumeDriver): super(LibvirtAOEVolumeDriver, self).__init__(connection, is_block_dev=True) - def _aoe_discover(self): - """Call aoe-discover (aoe-tools) AoE Discover.""" - (out, err) = utils.execute('aoe-discover', - run_as_root=True, check_exit_code=0) - return (out, err) - - def _aoe_revalidate(self, aoedev): - """Revalidate the LUN Geometry (When an AoE ID is reused).""" - (out, err) = utils.execute('aoe-revalidate', aoedev, - run_as_root=True, check_exit_code=0) - return (out, err) - - def _get_device_path(self, connection_info): - shelf = connection_info['data']['target_shelf'] - lun = connection_info['data']['target_lun'] - aoedev = 'e%s.%s' % (shelf, lun) - aoedevpath = '/dev/etherd/%s' % (aoedev) - return aoedevpath + # Call the factory here so we can support + # more than x86 architectures. + self.connector = connector.InitiatorConnector.factory( + 'AOE', utils._get_root_helper(), + device_scan_attempts=CONF.libvirt.num_aoe_discover_tries) def get_config(self, connection_info, disk_info): """Returns xml for libvirt.""" @@ -1053,48 +528,22 @@ class LibvirtAOEVolumeDriver(LibvirtBaseVolumeDriver): return conf def connect_volume(self, connection_info, mount_device): - shelf = connection_info['data']['target_shelf'] - lun = connection_info['data']['target_lun'] - aoedev = 'e%s.%s' % (shelf, lun) - aoedevpath = '/dev/etherd/%s' % (aoedev) + LOG.debug("Calling os-brick to attach AoE Volume") + device_info = self.connector.connect_volume(connection_info['data']) + LOG.debug("Attached AoE volume %s", device_info) - if os.path.exists(aoedevpath): - # NOTE(jbr_): If aoedevpath already exists, revalidate the LUN. - self._aoe_revalidate(aoedev) - else: - # NOTE(jbr_): If aoedevpath does not exist, do a discover. - self._aoe_discover() + connection_info['data']['device_path'] = device_info['path'] - # NOTE(jbr_): Device path is not always present immediately - def _wait_for_device_discovery(aoedevpath, mount_device): - tries = self.tries - if os.path.exists(aoedevpath): - raise loopingcall.LoopingCallDone() + def disconnect_volume(self, connection_info, disk_dev): + """Detach the volume from instance_name.""" - if self.tries >= CONF.libvirt.num_aoe_discover_tries: - raise exception.NovaException(_("AoE device not found at %s") % - (aoedevpath)) - LOG.warn(_LW("AoE volume not yet found at: %(aoedevpath)s. " - "Try number: %(tries)s"), - {'aoedevpath': aoedevpath, 'tries': tries}) + LOG.debug("calling os-brick to detach AoE Volume %s", + connection_info) + self.connector.disconnect_volume(connection_info['data'], None) + LOG.debug("Disconnected AoE Volume %s", disk_dev) - self._aoe_discover() - self.tries = self.tries + 1 - - self.tries = 0 - timer = loopingcall.FixedIntervalLoopingCall( - _wait_for_device_discovery, aoedevpath, mount_device) - timer.start(interval=2).wait() - - tries = self.tries - if tries != 0: - LOG.debug("Found AoE device %(aoedevpath)s " - "(after %(tries)s rediscover)", - {'aoedevpath': aoedevpath, - 'tries': tries}) - - connection_info['data']['device_path'] = \ - self._get_device_path(connection_info) + super(LibvirtAOEVolumeDriver, + self).disconnect_volume(connection_info, disk_dev) class LibvirtGlusterfsVolumeDriver(LibvirtBaseVolumeDriver): @@ -1199,26 +648,12 @@ class LibvirtFibreChannelVolumeDriver(LibvirtBaseVolumeDriver): super(LibvirtFibreChannelVolumeDriver, self).__init__(connection, is_block_dev=False) - def _get_pci_num(self, hba): - # NOTE(walter-boring) - # device path is in format of - # /sys/devices/pci0000:00/0000:00:03.0/0000:05:00.3/host2/fc_host/host2 - # sometimes an extra entry exists before the host2 value - # we always want the value prior to the host2 value - pci_num = None - if hba is not None: - if "device_path" in hba: - index = 0 - device_path = hba['device_path'].split('/') - for value in device_path: - if value.startswith('host'): - break - index = index + 1 - - if index > 0: - pci_num = device_path[index - 1] - - return pci_num + # Call the factory here so we can support + # more than x86 architectures. + self.connector = connector.InitiatorConnector.factory( + 'FIBRE_CHANNEL', utils._get_root_helper(), + use_multipath=CONF.libvirt.iscsi_use_multipath, + device_scan_attempts=CONF.libvirt.num_iscsi_scan_tries) def get_config(self, connection_info, disk_info): """Returns xml for libvirt.""" @@ -1229,193 +664,33 @@ class LibvirtFibreChannelVolumeDriver(LibvirtBaseVolumeDriver): conf.source_path = connection_info['data']['device_path'] return conf - def _get_lun_string_for_s390(self, lun): - target_lun = 0 - if lun < 256: - target_lun = "0x00%02x000000000000" % lun - elif lun <= 0xffffffff: - target_lun = "0x%08x00000000" % lun - return target_lun - - def _get_device_file_path_s390(self, pci_num, target_wwn, lun): - """Returns device file path""" - # NOTE the format of device file paths depends on the system - # architecture. Most architectures use a PCI based format. - # Systems following the S390, or S390x architecture use a format - # which is based upon the inherent channel architecture (ccw). - host_device = ("/dev/disk/by-path/ccw-%s-zfcp-%s:%s" % - (pci_num, - target_wwn, - lun)) - return host_device - - def _remove_lun_from_s390(self, connection_info): - """Rempove lun from s390 configuration""" - # If LUN scanning is turned off on systems following the s390, or - # s390x architecture LUNs need to be removed from the configuration - # using the unit_remove call. The unit_remove call needs to be issued - # for each (virtual) HBA and target_port. - fc_properties = connection_info['data'] - lun = int(fc_properties.get('target_lun', 0)) - target_lun = self._get_lun_string_for_s390(lun) - ports = fc_properties['target_wwn'] - - for device_num, target_wwn in self._get_possible_devices(ports): - libvirt_utils.perform_unit_remove_for_s390(device_num, - target_wwn, - target_lun) - - def _get_possible_devices(self, wwnports): - """Compute the possible valid fiber channel device options. - - :param wwnports: possible wwn addresses. Can either be string - or list of strings. - - :returns: list of (pci_id, wwn) tuples - - Given one or more wwn (mac addresses for fiber channel) ports - do the matrix math to figure out a set of pci device, wwn - tuples that are potentially valid (they won't all be). This - provides a search space for the device connection. - - """ - # the wwn (think mac addresses for fiber channel devices) can - # either be a single value or a list. Normalize it to a list - # for further operations. - wwns = [] - if isinstance(wwnports, list): - for wwn in wwnports: - wwns.append(str(wwn)) - elif isinstance(wwnports, six.string_types): - wwns.append(str(wwnports)) - - raw_devices = [] - hbas = libvirt_utils.get_fc_hbas_info() - for hba in hbas: - pci_num = self._get_pci_num(hba) - if pci_num is not None: - for wwn in wwns: - target_wwn = "0x%s" % wwn.lower() - raw_devices.append((pci_num, target_wwn)) - return raw_devices - - @utils.synchronized('connect_volume') def connect_volume(self, connection_info, disk_info): """Attach the volume to instance_name.""" - fc_properties = connection_info['data'] - mount_device = disk_info["dev"] - possible_devs = self._get_possible_devices(fc_properties['target_wwn']) - # map the raw device possibilities to possible host device paths - host_devices = [] - for device in possible_devs: - pci_num, target_wwn = device - if platform.machine() in (arch.S390, arch.S390X): - target_lun = self._get_lun_string_for_s390( - fc_properties.get('target_lun', 0)) - host_device = self._get_device_file_path_s390( - pci_num, - target_wwn, - target_lun) - libvirt_utils.perform_unit_add_for_s390( - pci_num, target_wwn, target_lun) - else: - host_device = ("/dev/disk/by-path/pci-%s-fc-%s-lun-%s" % - (pci_num, - target_wwn, - fc_properties.get('target_lun', 0))) - host_devices.append(host_device) + LOG.debug("Calling os-brick to attach FC Volume") + device_info = self.connector.connect_volume(connection_info['data']) + LOG.debug("Attached FC volume %s", device_info) - if len(host_devices) == 0: - # this is empty because we don't have any FC HBAs - msg = _("We are unable to locate any Fibre Channel devices") - raise exception.NovaException(msg) + connection_info['data']['device_path'] = device_info['path'] + if 'multipath_id' in device_info: + connection_info['data']['multipath_id'] = \ + device_info['multipath_id'] - # The /dev/disk/by-path/... node is not always present immediately - # We only need to find the first device. Once we see the first device - # multipath will have any others. - def _wait_for_device_discovery(host_devices, mount_device): - tries = self.tries - for device in host_devices: - LOG.debug("Looking for Fibre Channel dev %(device)s", - {'device': device}) - if os.path.exists(device): - self.host_device = device - # get the /dev/sdX device. This is used - # to find the multipath device. - self.device_name = os.path.realpath(device) - raise loopingcall.LoopingCallDone() - - if self.tries >= CONF.libvirt.num_iscsi_scan_tries: - msg = _("Fibre Channel device not found.") - raise exception.NovaException(msg) - - LOG.warn(_LW("Fibre volume not yet found at: %(mount_device)s. " - "Will rescan & retry. Try number: %(tries)s"), - {'mount_device': mount_device, 'tries': tries}) - - linuxscsi.rescan_hosts(libvirt_utils.get_fc_hbas_info()) - self.tries = self.tries + 1 - - self.host_device = None - self.device_name = None - self.tries = 0 - timer = loopingcall.FixedIntervalLoopingCall( - _wait_for_device_discovery, host_devices, mount_device) - timer.start(interval=2).wait() - - tries = self.tries - if self.host_device is not None and self.device_name is not None: - LOG.debug("Found Fibre Channel volume %(mount_device)s " - "(after %(tries)s rescans)", - {'mount_device': mount_device, - 'tries': tries}) - - # see if the new drive is part of a multipath - # device. If so, we'll use the multipath device. - mdev_info = linuxscsi.find_multipath_device(self.device_name) - if mdev_info is not None: - LOG.debug("Multipath device discovered %(device)s", - {'device': mdev_info['device']}) - device_path = mdev_info['device'] - connection_info['data']['device_path'] = device_path - connection_info['data']['devices'] = mdev_info['devices'] - connection_info['data']['multipath_id'] = mdev_info['id'] - else: - # we didn't find a multipath device. - # so we assume the kernel only sees 1 device - device_path = self.host_device - device_info = linuxscsi.get_device_info(self.device_name) - connection_info['data']['device_path'] = device_path - connection_info['data']['devices'] = [device_info] - - @utils.synchronized('connect_volume') - def disconnect_volume(self, connection_info, mount_device): + def disconnect_volume(self, connection_info, disk_dev): """Detach the volume from instance_name.""" + + LOG.debug("calling os-brick to detach FC Volume") + # TODO(walter-boring) eliminated the need for preserving + # multipath_id. Use scsi_id instead of multipath -ll + # This will then eliminate the need to pass anything in + # the 2nd param of disconnect_volume and be consistent + # with the rest of the connectors. + self.connector.disconnect_volume(connection_info['data'], + connection_info['data']) + LOG.debug("Disconnected FC Volume %s", disk_dev) + super(LibvirtFibreChannelVolumeDriver, - self).disconnect_volume(connection_info, mount_device) - - # If this is a multipath device, we need to search again - # and make sure we remove all the devices. Some of them - # might not have shown up at attach time. - if 'multipath_id' in connection_info['data']: - multipath_id = connection_info['data']['multipath_id'] - mdev_info = linuxscsi.find_multipath_device(multipath_id) - devices = mdev_info['devices'] if mdev_info else [] - LOG.debug("devices to remove = %s", devices) - else: - # only needed when multipath-tools work improperly - devices = connection_info['data'].get('devices', []) - LOG.warn(_LW("multipath-tools probably work improperly. " - "devices to remove = %s.") % devices) - - # There may have been more than 1 device mounted - # by the kernel for this volume. We have to remove - # all of them - for device in devices: - linuxscsi.remove_device(device) - if platform.machine() in (arch.S390, arch.S390X): - self._remove_lun_from_s390(connection_info) + self).disconnect_volume(connection_info, disk_dev) class LibvirtScalityVolumeDriver(LibvirtBaseVolumeDriver): diff --git a/nova/virt/volumeutils.py b/nova/virt/volumeutils.py index 75ad34177b..a8cdaf6d6c 100644 --- a/nova/virt/volumeutils.py +++ b/nova/virt/volumeutils.py @@ -15,21 +15,20 @@ """ Volume utilities for virt drivers. """ +from os_brick.initiator import connector +from oslo_concurrency import processutils as putils -from nova import exception from nova import utils -def get_iscsi_initiator(): +def get_iscsi_initiator(execute=None): """Get iscsi initiator name for this machine.""" - # NOTE(vish) openiscsi stores initiator name in a file that - # needs root permission to read. - try: - contents = utils.read_file_as_root('/etc/iscsi/initiatorname.iscsi') - except exception.FileNotFound: - return '' - for l in contents.split('\n'): - if l.startswith('InitiatorName='): - return l[l.index('=') + 1:].strip() - return '' + root_helper = utils._get_root_helper() + # so we can mock out the execute itself + # in unit tests. + if not execute: + execute = putils.execute + iscsi = connector.ISCSIConnector(root_helper=root_helper, + execute=execute) + return iscsi.get_initiator() diff --git a/requirements.txt b/requirements.txt index 3c958f605e..d1b49c4cdb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,3 +51,4 @@ oslo.middleware>=2.4.0 # Apache-2.0 psutil<2.0.0,>=1.1.1 oslo.versionedobjects!=0.5.0,>=0.3.0 alembic>=0.7.2 +os-brick>=0.3.2 # Apache-2.0