Add Windows Fibre Channel connector
This patch adds a Windows Fibre Channel connector. The patch using Windows os-brick connectors in the Hyper-V Nova driver: https://review.openstack.org/#/c/273504/ Change-Id: Iec263e5d5803fcceb315e17d16d2b154e0214584 Partial-Implements: blueprint os-brick-windows-support
This commit is contained in:
parent
4045300fa9
commit
25453f3a2b
@ -92,6 +92,7 @@ connector_list = [
|
||||
'os_brick.initiator.connectors.disco.DISCOConnector',
|
||||
'os_brick.initiator.windows.base.BaseWindowsConnector',
|
||||
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
|
||||
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
|
||||
'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector',
|
||||
]
|
||||
|
||||
@ -147,6 +148,8 @@ _connector_mapping_linux_s390x = {
|
||||
_connector_mapping_windows = {
|
||||
initiator.ISCSI:
|
||||
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
|
||||
initiator.FIBRE_CHANNEL:
|
||||
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
|
||||
initiator.SMBFS:
|
||||
'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector',
|
||||
}
|
||||
|
127
os_brick/initiator/windows/fibre_channel.py
Normal file
127
os_brick/initiator/windows/fibre_channel.py
Normal file
@ -0,0 +1,127 @@
|
||||
# Copyright 2016 Cloudbase Solutions Srl
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
|
||||
from os_win import utilsfactory
|
||||
from oslo_log import log as logging
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.initiator.windows import base as win_conn_base
|
||||
from os_brick import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WindowsFCConnector(win_conn_base.BaseWindowsConnector):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WindowsFCConnector, self).__init__(*args, **kwargs)
|
||||
self._fc_utils = utilsfactory.get_fc_utils()
|
||||
|
||||
@staticmethod
|
||||
def get_connector_properties(*args, **kwargs):
|
||||
props = {}
|
||||
|
||||
fc_utils = utilsfactory.get_fc_utils()
|
||||
fc_utils.refresh_hba_configuration()
|
||||
fc_hba_ports = fc_utils.get_fc_hba_ports()
|
||||
|
||||
if fc_hba_ports:
|
||||
wwnns = []
|
||||
wwpns = []
|
||||
for port in fc_hba_ports:
|
||||
wwnns.append(port['node_name'])
|
||||
wwpns.append(port['port_name'])
|
||||
props['wwpns'] = wwpns
|
||||
props['wwnns'] = list(set(wwnns))
|
||||
return props
|
||||
|
||||
@utils.trace
|
||||
def connect_volume(self, connection_properties):
|
||||
volume_paths = self.get_volume_paths(connection_properties)
|
||||
if not volume_paths:
|
||||
raise exception.NoFibreChannelVolumeDeviceFound()
|
||||
|
||||
device_path = volume_paths[0]
|
||||
device_number = self._diskutils.get_device_number_from_device_name(
|
||||
device_path)
|
||||
scsi_wwn = self._get_scsi_wwn(device_number)
|
||||
device_info = {'type': 'block',
|
||||
'path': device_path,
|
||||
'number': device_number,
|
||||
'scsi_wwn': scsi_wwn}
|
||||
return device_info
|
||||
|
||||
@utils.trace
|
||||
def get_volume_paths(self, connection_properties):
|
||||
# Returns a list containing at most one disk path such as
|
||||
# \\.\PhysicalDrive4.
|
||||
#
|
||||
# If multipath is used and the MPIO service is properly configured
|
||||
# to claim the disks, we'll still get a single device path, having
|
||||
# the same format, which will be used for all the IO operations.
|
||||
disk_paths = set()
|
||||
|
||||
for attempt in range(self.device_scan_attempts):
|
||||
self._diskutils.rescan_disks()
|
||||
volume_mappings = self._get_fc_volume_mappings(
|
||||
connection_properties)
|
||||
LOG.debug("Retrieved volume mappings %(vol_mappings)s "
|
||||
"for volume %(conn_props)s",
|
||||
dict(vol_mappings=volume_mappings,
|
||||
conn_props=connection_properties))
|
||||
|
||||
# Because of MPIO, we may not be able to get the device name
|
||||
# from a specific mapping if the disk was accessed through
|
||||
# an other HBA at that moment. In that case, the device name
|
||||
# will show up as an empty string.
|
||||
for mapping in volume_mappings:
|
||||
device_name = mapping['device_name']
|
||||
if device_name:
|
||||
disk_paths.add(device_name)
|
||||
|
||||
if disk_paths:
|
||||
break
|
||||
|
||||
self._check_device_paths(disk_paths)
|
||||
return list(disk_paths)
|
||||
|
||||
def _get_fc_volume_mappings(self, connection_properties):
|
||||
# Note(lpetrut): All the WWNs returned by os-win are upper case.
|
||||
target_wwpns = [wwpn.upper()
|
||||
for wwpn in connection_properties['target_wwn']]
|
||||
target_lun = connection_properties['target_lun']
|
||||
|
||||
volume_mappings = []
|
||||
hba_mappings = self._get_fc_hba_mappings()
|
||||
for node_name in hba_mappings:
|
||||
target_mappings = self._fc_utils.get_fc_target_mappings(node_name)
|
||||
for mapping in target_mappings:
|
||||
if (mapping['port_name'] in target_wwpns
|
||||
and mapping['lun'] == target_lun):
|
||||
volume_mappings.append(mapping)
|
||||
|
||||
return volume_mappings
|
||||
|
||||
def _get_fc_hba_mappings(self):
|
||||
mappings = collections.defaultdict(list)
|
||||
fc_hba_ports = self._fc_utils.get_fc_hba_ports()
|
||||
for port in fc_hba_ports:
|
||||
mappings[port['node_name']].append(port['port_name'])
|
||||
return mappings
|
||||
|
||||
@utils.trace
|
||||
def disconnect_volume(self, connection_properties):
|
||||
pass
|
@ -18,6 +18,7 @@ import mock
|
||||
|
||||
from os_brick import initiator
|
||||
from os_brick.initiator import connector
|
||||
from os_brick.initiator.windows import fibre_channel
|
||||
from os_brick.initiator.windows import iscsi
|
||||
from os_brick.initiator.windows import smbfs
|
||||
from os_brick.tests.windows import test_base
|
||||
@ -27,6 +28,8 @@ from os_brick.tests.windows import test_base
|
||||
class WindowsConnectorFactoryTestCase(test_base.WindowsConnectorTestBase):
|
||||
@ddt.data({'proto': initiator.ISCSI,
|
||||
'expected_cls': iscsi.WindowsISCSIConnector},
|
||||
{'proto': initiator.FIBRE_CHANNEL,
|
||||
'expected_cls': fibre_channel.WindowsFCConnector},
|
||||
{'proto': initiator.SMBFS,
|
||||
'expected_cls': smbfs.WindowsSMBFSConnector})
|
||||
@ddt.unpack
|
||||
|
154
os_brick/tests/windows/test_fibre_channel.py
Normal file
154
os_brick/tests/windows/test_fibre_channel.py
Normal file
@ -0,0 +1,154 @@
|
||||
# Copyright 2016 Cloudbase Solutions Srl
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.initiator.windows import fibre_channel as fc
|
||||
from os_brick.tests.windows import test_base
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class WindowsFCConnectorTestCase(test_base.WindowsConnectorTestBase):
|
||||
def setUp(self):
|
||||
super(WindowsFCConnectorTestCase, self).setUp()
|
||||
self._connector = fc.WindowsFCConnector()
|
||||
|
||||
self._diskutils = self._connector._diskutils
|
||||
self._fc_utils = self._connector._fc_utils
|
||||
|
||||
@ddt.data(True, False)
|
||||
@mock.patch.object(fc.utilsfactory, 'get_fc_utils')
|
||||
def test_get_volume_connector_props(self, valid_fc_hba_ports,
|
||||
mock_get_fc_utils):
|
||||
fake_fc_hba_ports = [{'node_name': mock.sentinel.node_name,
|
||||
'port_name': mock.sentinel.port_name},
|
||||
{'node_name': mock.sentinel.second_node_name,
|
||||
'port_name': mock.sentinel.second_port_name}]
|
||||
self._fc_utils = mock_get_fc_utils.return_value
|
||||
self._fc_utils.get_fc_hba_ports.return_value = (
|
||||
fake_fc_hba_ports if valid_fc_hba_ports else [])
|
||||
|
||||
props = self._connector.get_connector_properties()
|
||||
|
||||
self._fc_utils.refresh_hba_configuration.assert_called_once_with()
|
||||
self._fc_utils.get_fc_hba_ports.assert_called_once_with()
|
||||
|
||||
if valid_fc_hba_ports:
|
||||
expected_props = {
|
||||
'wwpns': [mock.sentinel.port_name,
|
||||
mock.sentinel.second_port_name],
|
||||
'wwnns': [mock.sentinel.node_name,
|
||||
mock.sentinel.second_node_name]
|
||||
}
|
||||
else:
|
||||
expected_props = {}
|
||||
|
||||
self.assertItemsEqual(expected_props, props)
|
||||
|
||||
@mock.patch.object(fc.WindowsFCConnector, '_get_scsi_wwn')
|
||||
@mock.patch.object(fc.WindowsFCConnector, 'get_volume_paths')
|
||||
def test_connect_volume(self, mock_get_vol_paths,
|
||||
mock_get_scsi_wwn):
|
||||
mock_get_vol_paths.return_value = [mock.sentinel.dev_name]
|
||||
mock_get_dev_num = self._diskutils.get_device_number_from_device_name
|
||||
mock_get_dev_num.return_value = mock.sentinel.dev_num
|
||||
|
||||
expected_device_info = dict(type='block',
|
||||
path=mock.sentinel.dev_name,
|
||||
number=mock.sentinel.dev_num,
|
||||
scsi_wwn=mock_get_scsi_wwn.return_value)
|
||||
device_info = self._connector.connect_volume(mock.sentinel.conn_props)
|
||||
|
||||
self.assertEqual(expected_device_info, device_info)
|
||||
mock_get_vol_paths.assert_called_once_with(mock.sentinel.conn_props)
|
||||
mock_get_dev_num.assert_called_once_with(mock.sentinel.dev_name)
|
||||
mock_get_scsi_wwn.assert_called_once_with(mock.sentinel.dev_num)
|
||||
|
||||
@mock.patch.object(fc.WindowsFCConnector, 'get_volume_paths')
|
||||
def test_connect_volume_not_found(self, mock_get_vol_paths):
|
||||
mock_get_vol_paths.return_value = []
|
||||
self.assertRaises(exception.NoFibreChannelVolumeDeviceFound,
|
||||
self._connector.connect_volume,
|
||||
mock.sentinel.conn_props)
|
||||
|
||||
@ddt.data({'volume_mappings': [], 'expected_paths': []},
|
||||
{'volume_mappings': [dict(device_name='')] * 3,
|
||||
'expected_paths': []},
|
||||
{'volume_mappings': [dict(device_name=''),
|
||||
dict(device_name=mock.sentinel.disk_path)],
|
||||
'expected_paths': [mock.sentinel.disk_path]})
|
||||
@ddt.unpack
|
||||
@mock.patch.object(fc.WindowsFCConnector, '_get_fc_volume_mappings')
|
||||
@mock.patch.object(fc.WindowsFCConnector, '_check_device_paths')
|
||||
def test_get_volume_paths(self, mock_check_device_paths,
|
||||
mock_get_fc_mappings,
|
||||
volume_mappings, expected_paths):
|
||||
mock_get_fc_mappings.return_value = volume_mappings
|
||||
|
||||
vol_paths = self._connector.get_volume_paths(mock.sentinel.conn_props)
|
||||
self.assertEqual(expected_paths, vol_paths)
|
||||
|
||||
# In this test case, either the volume is found after the first
|
||||
# attempt, either it's not found at all, in which case we'd expect
|
||||
# the number of retries to be the requested maximum number of rescans.
|
||||
expected_try_count = (1 if expected_paths
|
||||
else self._connector.device_scan_attempts)
|
||||
self._diskutils.rescan_disks.assert_has_calls(
|
||||
[mock.call()] * expected_try_count)
|
||||
mock_get_fc_mappings.assert_has_calls(
|
||||
[mock.call(mock.sentinel.conn_props)] * expected_try_count)
|
||||
mock_check_device_paths.assert_called_once_with(
|
||||
set(vol_paths))
|
||||
|
||||
@mock.patch.object(fc.WindowsFCConnector, '_get_fc_hba_mappings')
|
||||
def test_get_fc_volume_mappings(self, mock_get_fc_hba_mappings):
|
||||
fake_target_wwpn = 'FAKE_TARGET_WWPN'
|
||||
fake_conn_props = dict(target_lun=mock.sentinel.target_lun,
|
||||
target_wwn=[fake_target_wwpn])
|
||||
|
||||
mock_hba_mappings = {mock.sentinel.node_name: mock.sentinel.hba_ports}
|
||||
mock_get_fc_hba_mappings.return_value = mock_hba_mappings
|
||||
|
||||
all_target_mappings = [{'device_name': mock.sentinel.dev_name,
|
||||
'port_name': fake_target_wwpn,
|
||||
'lun': mock.sentinel.target_lun},
|
||||
{'device_name': mock.sentinel.dev_name_1,
|
||||
'port_name': mock.sentinel.target_port_name_1,
|
||||
'lun': mock.sentinel.target_lun},
|
||||
{'device_name': mock.sentinel.dev_name,
|
||||
'port_name': mock.sentinel.target_port_name,
|
||||
'lun': mock.sentinel.target_lun_1}]
|
||||
expected_mappings = [all_target_mappings[0]]
|
||||
|
||||
self._fc_utils.get_fc_target_mappings.return_value = (
|
||||
all_target_mappings)
|
||||
|
||||
volume_mappings = self._connector._get_fc_volume_mappings(
|
||||
fake_conn_props)
|
||||
self.assertEqual(expected_mappings, volume_mappings)
|
||||
|
||||
def test_get_fc_hba_mappings(self):
|
||||
fake_fc_hba_ports = [{'node_name': mock.sentinel.node_name,
|
||||
'port_name': mock.sentinel.port_name}]
|
||||
|
||||
self._fc_utils.get_fc_hba_ports.return_value = fake_fc_hba_ports
|
||||
|
||||
resulted_mappings = self._connector._get_fc_hba_mappings()
|
||||
|
||||
expected_mappings = {
|
||||
mock.sentinel.node_name: [mock.sentinel.port_name]}
|
||||
self.assertEqual(expected_mappings, resulted_mappings)
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Add Windows Fibre Channel connector support.
|
Loading…
Reference in New Issue
Block a user