Add support for FC zone lifecycle management

Allows automated zone lifecycle management in the attach/detach
entry points of the volume manager for FC volumes (when fabric
zoning is enabled). Zones are configured automatically as part
of the active zone set in the FC SAN to provide a more flexible
and secure way of controlling access. FC zone manager enables
SAN vendors to add support for pluggable implementations.

Change-Id: I86e02e11afec71ffa25dfbcbd0dc3ddcc35741ea
Implements: blueprint cinder-fc-zone-manager
This commit is contained in:
Santhoshkumar Kolathur 2013-08-30 19:48:52 -07:00
parent c2807f0ed4
commit d8fcf93c91
20 changed files with 3040 additions and 2 deletions

View File

@ -689,3 +689,20 @@ class HPMSAConnectionError(HPMSAVolumeDriverException):
class HPMSANotEnoughSpace(HPMSAVolumeDriverException): class HPMSANotEnoughSpace(HPMSAVolumeDriverException):
message = _("Not enough space on VDisk (%(vdisk)s)") message = _("Not enough space on VDisk (%(vdisk)s)")
# Fibre Channel Zone Manager
class ZoneManagerException(CinderException):
message = _("Fibre Channel connection control failure: %(reason)s")
class FCZoneDriverException(CinderException):
message = _("Fibre Channel Zone operation failed: %(reason)s")
class FCSanLookupServiceException(CinderException):
message = _("Fibre Channel SAN Lookup failure: %(reason)s")
class BrocadeZoningCliException(CinderException):
message = _("Fibre Channel Zoning CLI error: %(reason)s")

View File

@ -0,0 +1,142 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Unit tests for brcd fc san lookup service."""
import mock
import paramiko
from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import log as logging
from cinder import test
from cinder.volume import configuration as conf
from cinder.zonemanager.drivers.brocade.brcd_fc_san_lookup_service \
import BrcdFCSanLookupService
import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant
from mock import patch
LOG = logging.getLogger(__name__)
nsshow = '20:1a:00:05:1e:e8:e3:29'
switch_data = [' N 011a00;2,3;20:1a:00:05:1e:e8:e3:29;\
20:1a:00:05:1e:e8:e3:29;na']
nsshow_data = ['10:00:8c:7c:ff:52:3b:01', '20:24:00:02:ac:00:0a:50']
_device_map_to_verify = {
'100000051e55a100': {
'initiator_port_wwn_list': ['10008c7cff523b01'],
'target_port_wwn_list': ['20240002ac000a50']}}
class TestBrcdFCSanLookupService(BrcdFCSanLookupService, test.TestCase):
def setUp(self):
super(TestBrcdFCSanLookupService, self).setUp()
self.client = paramiko.SSHClient()
self.configuration = conf.Configuration(None)
self.configuration.set_default('fc_fabric_names', 'BRCD_FAB_2')
self.create_configuration()
# override some of the functions
def __init__(self, *args, **kwargs):
test.TestCase.__init__(self, *args, **kwargs)
def create_configuration(self):
fc_fabric_opts = []
fc_fabric_opts.append(cfg.StrOpt('fc_fabric_address_BRCD_FAB_2',
default='10.24.49.100', help=''))
fc_fabric_opts.append(cfg.StrOpt('fc_fabric_user_BRCD_FAB_2',
default='admin', help=''))
fc_fabric_opts.append(cfg.StrOpt('fc_fabric_password_BRCD_FAB_2',
default='password', help='',
secret=True))
fc_fabric_opts.append(cfg.IntOpt('fc_fabric_port_BRCD_FAB_2',
default=22, help=''))
fc_fabric_opts.append(cfg.StrOpt('principal_switch_wwn_BRCD_FAB_2',
default='100000051e55a100', help=''))
self.configuration.append_config_values(fc_fabric_opts)
@patch.object(BrcdFCSanLookupService, 'get_nameserver_info')
def test_get_device_mapping_from_network(self, get_nameserver_info_mock):
initiator_list = ['10008c7cff523b01']
target_list = ['20240002ac000a50', '20240002ac000a40']
with mock.patch.object(self.client, 'connect') \
as client_connect_mock:
get_nameserver_info_mock.return_value = (nsshow_data)
device_map = self.get_device_mapping_from_network(
initiator_list, target_list)
self.assertDictMatch(device_map, _device_map_to_verify)
@patch.object(BrcdFCSanLookupService, '_get_switch_data')
def test_get_nameserver_info(self, get_switch_data_mock):
ns_info_list = []
ns_info_list_expected = ['20:1a:00:05:1e:e8:e3:29',
'20:1a:00:05:1e:e8:e3:29']
get_switch_data_mock.return_value = (switch_data)
ns_info_list = self.get_nameserver_info()
self.assertEqual(ns_info_list, ns_info_list_expected)
def test__get_switch_data(self):
cmd = ZoneConstant.NS_SHOW
with mock.patch.object(self.client, 'exec_command') \
as exec_command_mock:
exec_command_mock.return_value = (Stream(),
Stream(nsshow),
Stream())
switch_data = self._get_switch_data(cmd)
self.assertEqual(switch_data, nsshow)
exec_command_mock.assert_called_once_with(cmd)
def test__parse_ns_output(self):
invalid_switch_data = [' N 011a00;20:1a:00:05:1e:e8:e3:29']
return_wwn_list = []
expected_wwn_list = ['20:1a:00:05:1e:e8:e3:29']
return_wwn_list = self._parse_ns_output(switch_data)
self.assertEqual(return_wwn_list, expected_wwn_list)
self.assertRaises(exception.InvalidParameterValue,
self._parse_ns_output, invalid_switch_data)
def test_get_formatted_wwn(self):
wwn_list = ['10008c7cff523b01']
return_wwn_list = []
expected_wwn_list = ['10:00:8c:7c:ff:52:3b:01']
return_wwn_list.append(self.get_formatted_wwn(wwn_list[0]))
self.assertEqual(return_wwn_list, expected_wwn_list)
class Channel(object):
def recv_exit_status(self):
return 0
class Stream(object):
def __init__(self, buffer=''):
self.buffer = buffer
self.channel = Channel()
def readlines(self):
return self.buffer
def close(self):
pass
def flush(self):
self.buffer = ''

View File

@ -0,0 +1,291 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Unit tests for brcd fc zone client cli."""
import mock
from cinder import exception
from cinder.openstack.common import log as logging
from cinder.openstack.common import processutils
from cinder import test
from cinder.zonemanager.drivers.brocade.brcd_fc_zone_client_cli \
import BrcdFCZoneClientCLI
import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant
from mock import patch
LOG = logging.getLogger(__name__)
nsshow = '20:1a:00:05:1e:e8:e3:29'
switch_data = [' N 011a00;2,3;20:1a:00:05:1e:e8:e3:29;\
20:1a:00:05:1e:e8:e3:29;na',
' Fabric Port Name: 20:1a:00:05:1e:e8:e3:29']
cfgactvshow = ['Effective configuration:\n',
' cfg:\tOpenStack_Cfg\t\n',
' zone:\topenstack50060b0000c26604201900051ee8e329\t\n',
'\t\t50:06:0b:00:00:c2:66:04\n',
'\t\t20:19:00:05:1e:e8:e3:29\n']
active_zoneset = {
'zones': {
'openstack50060b0000c26604201900051ee8e329':
['50:06:0b:00:00:c2:66:04', '20:19:00:05:1e:e8:e3:29']},
'active_zone_config': 'OpenStack_Cfg'}
active_zoneset_multiple_zones = {
'zones': {
'openstack50060b0000c26604201900051ee8e329':
['50:06:0b:00:00:c2:66:04', '20:19:00:05:1e:e8:e3:29'],
'openstack50060b0000c26602201900051ee8e327':
['50:06:0b:00:00:c2:66:02', '20:19:00:05:1e:e8:e3:27']},
'active_zone_config': 'OpenStack_Cfg'}
new_zone = {'openstack10000012345678902001009876543210':
['10:00:00:12:34:56:78:90', '20:01:00:98:76:54:32:10']}
new_zones = {'openstack10000012345678902001009876543210':
['10:00:00:12:34:56:78:90', '20:01:00:98:76:54:32:10'],
'openstack10000011111111112001001111111111':
['10:00:00:11:11:11:11:11', '20:01:00:11:11:11:11:11']}
zone_names_to_delete = 'openstack50060b0000c26604201900051ee8e329'
supported_firmware = ['Kernel: 2.6', 'Fabric OS: v7.0.1']
unsupported_firmware = ['Fabric OS: v6.2.1']
class TestBrcdFCZoneClientCLI(BrcdFCZoneClientCLI, test.TestCase):
def setUp(self):
super(TestBrcdFCZoneClientCLI, self).setUp()
# override some of the functions
def __init__(self, *args, **kwargs):
test.TestCase.__init__(self, *args, **kwargs)
@patch.object(BrcdFCZoneClientCLI, '_get_switch_info')
def test_get_active_zone_set(self, get_switch_info_mock):
cmd_list = [ZoneConstant.GET_ACTIVE_ZONE_CFG]
get_switch_info_mock.return_value = cfgactvshow
active_zoneset_returned = self.get_active_zone_set()
get_switch_info_mock.assert_called_once_with(cmd_list)
self.assertDictMatch(active_zoneset_returned, active_zoneset)
@patch.object(BrcdFCZoneClientCLI, '_run_ssh')
def test_get_active_zone_set_ssh_error(self, run_ssh_mock):
run_ssh_mock.side_effect = processutils.ProcessExecutionError
self.assertRaises(exception.BrocadeZoningCliException,
self.get_active_zone_set)
@mock.patch.object(BrcdFCZoneClientCLI, 'get_active_zone_set')
@mock.patch.object(BrcdFCZoneClientCLI, 'apply_zone_change')
@mock.patch.object(BrcdFCZoneClientCLI, '_cfg_save')
def test_add_zones_new_zone_no_activate(self, get_active_zs_mock,
apply_zone_change_mock,
cfg_save_mock):
get_active_zs_mock.return_value = active_zoneset
self.add_zones(new_zones, False)
get_active_zs_mock.assert_called_once()
apply_zone_change_mock.assert_called_twice()
cfg_save_mock.assert_called_once()
@mock.patch.object(BrcdFCZoneClientCLI, 'get_active_zone_set')
@mock.patch.object(BrcdFCZoneClientCLI, 'apply_zone_change')
@mock.patch.object(BrcdFCZoneClientCLI, '_cfg_save')
@mock.patch.object(BrcdFCZoneClientCLI, 'activate_zoneset')
def test_add_zones_new_zone_activate(self, get_active_zs_mock,
apply_zone_change_mock,
cfg_save_mock,
activate_zoneset_mock):
get_active_zs_mock.return_value = active_zoneset
self.add_zones(new_zone, True)
get_active_zs_mock.assert_called_once()
apply_zone_change_mock.assert_called_once()
cfg_save_mock.assert_called_once()
activate_zoneset_mock.assert_called_once()
@mock.patch.object(BrcdFCZoneClientCLI, '_ssh_execute')
def test_activate_zoneset(self, ssh_execute_mock):
ssh_execute_mock.return_value = True
return_value = self.activate_zoneset('zoneset1')
self.assertTrue(return_value)
@mock.patch.object(BrcdFCZoneClientCLI, '_ssh_execute')
def test_deactivate_zoneset(self, ssh_execute_mock):
ssh_execute_mock.return_value = True
return_value = self.deactivate_zoneset()
self.assertTrue(return_value)
@mock.patch.object(BrcdFCZoneClientCLI, 'get_active_zone_set')
@mock.patch.object(BrcdFCZoneClientCLI, 'apply_zone_change')
@mock.patch.object(BrcdFCZoneClientCLI, '_cfg_save')
def test_delete_zones_activate_false(self, get_active_zs_mock,
apply_zone_change_mock,
cfg_save_mock):
get_active_zs_mock.return_value = active_zoneset_multiple_zones
with mock.patch.object(self, '_zone_delete') \
as zone_delete_mock:
self.delete_zones(zone_names_to_delete, False)
get_active_zs_mock.assert_called_once()
apply_zone_change_mock.assert_called_once()
zone_delete_mock.assert_called_once_with(zone_names_to_delete)
cfg_save_mock.assert_called_once()
@patch.object(BrcdFCZoneClientCLI, 'get_active_zone_set')
@patch.object(BrcdFCZoneClientCLI, 'apply_zone_change')
@patch.object(BrcdFCZoneClientCLI, '_cfg_save')
@patch.object(BrcdFCZoneClientCLI, 'activate_zoneset')
def test_delete_zones_activate_true(self, get_active_zs_mock,
apply_zone_change_mock,
cfg_save_mock,
activate_zs_mock):
get_active_zs_mock.return_value = active_zoneset_multiple_zones
with mock.patch.object(self, '_zone_delete') \
as zone_delete_mock:
self.delete_zones(zone_names_to_delete, True)
get_active_zs_mock.assert_called_once()
apply_zone_change_mock.assert_called_once()
zone_delete_mock.assert_called_once_with(zone_names_to_delete)
cfg_save_mock.assert_called_once()
activate_zs_mock.assert_called_once()
@patch.object(BrcdFCZoneClientCLI, '_get_switch_info')
def test_get_nameserver_info(self, get_switch_info_mock):
ns_info_list = []
ns_info_list_expected = ['20:1a:00:05:1e:e8:e3:29',
'20:1a:00:05:1e:e8:e3:29']
get_switch_info_mock.return_value = (switch_data)
ns_info_list = self.get_nameserver_info()
self.assertEqual(ns_info_list, ns_info_list_expected)
@patch.object(BrcdFCZoneClientCLI, '_run_ssh')
def test_get_nameserver_info_ssh_error(self, run_ssh_mock):
run_ssh_mock.side_effect = processutils.ProcessExecutionError
self.assertRaises(exception.BrocadeZoningCliException,
self.get_nameserver_info)
@patch.object(BrcdFCZoneClientCLI, '_ssh_execute')
def test__cfg_save(self, ssh_execute_mock):
cmd_list = [ZoneConstant.CFG_SAVE]
self._cfg_save()
ssh_execute_mock.assert_called_once_with(cmd_list, True, 1)
@patch.object(BrcdFCZoneClientCLI, 'apply_zone_change')
def test__zone_delete(self, apply_zone_change_mock):
zone_name = 'testzone'
cmd_list = ['zonedelete', '"testzone"']
self._zone_delete(zone_name)
apply_zone_change_mock.assert_called_once_with(cmd_list)
@patch.object(BrcdFCZoneClientCLI, 'apply_zone_change')
def test__cfg_trans_abort(self, apply_zone_change_mock):
cmd_list = [ZoneConstant.CFG_ZONE_TRANS_ABORT]
with mock.patch.object(self, '_is_trans_abortable') \
as is_trans_abortable_mock:
is_trans_abortable_mock.return_value = True
self._cfg_trans_abort()
is_trans_abortable_mock.assert_called_once()
apply_zone_change_mock.assert_called_once_with(cmd_list)
@patch.object(BrcdFCZoneClientCLI, '_run_ssh')
def test__is_trans_abortable_true(self, run_ssh_mock):
cmd_list = [ZoneConstant.CFG_SHOW_TRANS]
run_ssh_mock.return_value = (Stream(ZoneConstant.TRANS_ABORTABLE),
None)
data = self._is_trans_abortable()
self.assertTrue(data)
run_ssh_mock.assert_called_once_with(cmd_list, True, 1)
@patch.object(BrcdFCZoneClientCLI, '_run_ssh')
def test__is_trans_abortable_ssh_error(self, run_ssh_mock):
run_ssh_mock.return_value = (Stream(), Stream())
self.assertRaises(exception.BrocadeZoningCliException,
self._is_trans_abortable)
@patch.object(BrcdFCZoneClientCLI, '_run_ssh')
def test__is_trans_abortable_false(self, run_ssh_mock):
cmd_list = [ZoneConstant.CFG_SHOW_TRANS]
cfgtransshow = 'There is no outstanding zoning transaction'
run_ssh_mock.return_value = (Stream(cfgtransshow), None)
data = self._is_trans_abortable()
self.assertFalse(data)
run_ssh_mock.assert_called_once_with(cmd_list, True, 1)
@patch.object(BrcdFCZoneClientCLI, '_run_ssh')
def test_apply_zone_change(self, run_ssh_mock):
cmd_list = [ZoneConstant.CFG_SAVE]
run_ssh_mock.return_value = (None, None)
self.apply_zone_change(cmd_list)
run_ssh_mock.assert_called_once_with(cmd_list, True, 1)
@patch.object(BrcdFCZoneClientCLI, '_run_ssh')
def test__get_switch_info(self, run_ssh_mock):
cmd_list = [ZoneConstant.NS_SHOW]
nsshow_list = [nsshow]
run_ssh_mock.return_value = (Stream(nsshow), Stream())
switch_data = self._get_switch_info(cmd_list)
self.assertEqual(switch_data, nsshow_list)
run_ssh_mock.assert_called_once_with(cmd_list, True, 1)
def test__parse_ns_output(self):
invalid_switch_data = [' N 011a00;20:1a:00:05:1e:e8:e3:29']
return_wwn_list = []
expected_wwn_list = ['20:1a:00:05:1e:e8:e3:29']
return_wwn_list = self._parse_ns_output(switch_data)
self.assertEqual(return_wwn_list, expected_wwn_list)
self.assertRaises(exception.InvalidParameterValue,
self._parse_ns_output, invalid_switch_data)
@patch.object(BrcdFCZoneClientCLI, '_execute_shell_cmd')
def test_is_supported_firmware(self, exec_shell_cmd_mock):
exec_shell_cmd_mock.return_value = (supported_firmware, None)
self.assertTrue(self.is_supported_firmware())
@patch.object(BrcdFCZoneClientCLI, '_execute_shell_cmd')
def test_is_supported_firmware_invalid(self, exec_shell_cmd_mock):
exec_shell_cmd_mock.return_value = (unsupported_firmware, None)
self.assertFalse(self.is_supported_firmware())
@patch.object(BrcdFCZoneClientCLI, '_execute_shell_cmd')
def test_is_supported_firmware_no_ssh_response(self, exec_shell_cmd_mock):
exec_shell_cmd_mock.return_value = (None, Stream())
self.assertFalse(self.is_supported_firmware())
@patch.object(BrcdFCZoneClientCLI, '_execute_shell_cmd')
def test_is_supported_firmware_ssh_error(self, exec_shell_cmd_mock):
exec_shell_cmd_mock.side_effect = processutils.ProcessExecutionError
self.assertRaises(exception.BrocadeZoningCliException,
self.is_supported_firmware)
class Channel(object):
def recv_exit_status(self):
return 0
class Stream(object):
def __init__(self, buffer=''):
self.buffer = buffer
self.channel = Channel()
def readlines(self):
return self.buffer
def splitlines(self):
return self.buffer.splitlines()
def close(self):
pass
def flush(self):
self.buffer = ''

View File

@ -0,0 +1,233 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Unit tests for Brocade fc zone driver."""
import paramiko
from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import importutils
from cinder.openstack.common import log as logging
from cinder import test
from cinder.volume import configuration as conf
LOG = logging.getLogger(__name__)
_active_cfg_before_add = {}
_active_cfg_before_delete = {
'zones': {
'openstack10008c7cff523b0120240002ac000a50': (
['10:00:8c:7c:ff:52:3b:01',
'20:24:00:02:ac:00:0a:50']), 't_zone': ['1,0']},
'active_zone_config': 'cfg1'}
_activate = True
_zone_name = 'openstack10008c7cff523b0120240002ac000a50'
_target_ns_map = {'100000051e55a100': ['20240002ac000a50']}
_initiator_ns_map = {'100000051e55a100': ['10008c7cff523b01']}
_zone_map_to_add = {'openstack10008c7cff523b0120240002ac000a50': (
['10:00:8c:7c:ff:52:3b:01', '20:24:00:02:ac:00:0a:50'])}
_initiator_target_map = {'10008c7cff523b01': ['20240002ac000a50']}
_device_map_to_verify = {
'100000051e55a100': {
'initiator_port_wwn_list': [
'10008c7cff523b01'], 'target_port_wwn_list': ['20240002ac000a50']}}
_fabric_wwn = '100000051e55a100'
class BrcdFcZoneDriverBaseTest(object):
def setup_config(self, is_normal, mode):
fc_test_opts = [
cfg.StrOpt('fc_fabric_address_BRCD_FAB_1', default='10.24.48.213',
help='FC Fabric names'),
]
configuration = conf.Configuration(fc_test_opts)
# fill up config
configuration.zoning_mode = 'fabric'
configuration.zone_driver = ('cinder.tests.test_brcd_fc_zone_driver'
'.FakeBrcdFCZoneDriver')
configuration.brcd_sb_connector = ('cinder.tests.'
'test_brcd_fc_zone_driver'
'.FakeBrcdFCZoneClientCLI')
configuration.zoning_policy = 'initiator-target'
configuration.zone_activate = True
configuration.zone_name_prefix = 'openstack'
configuration.fc_san_lookup_service = ('cinder.tests.'
'test_brcd_fc_zone_driver.'
'FakeBrcdFCSanLookupService')
configuration.fc_fabric_names = 'BRCD_FAB_1'
configuration.fc_fabric_address_BRCD_FAB_1 = '10.24.48.213'
if (is_normal):
configuration.fc_fabric_user_BRCD_FAB_1 = 'admin'
else:
configuration.fc_fabric_user_BRCD_FAB_1 = 'invaliduser'
configuration.fc_fabric_password_BRCD_FAB_1 = 'password'
if (mode == 1):
configuration.zoning_policy_BRCD_FAB_1 = 'initiator-target'
elif (mode == 2):
configuration.zoning_policy_BRCD_FAB_1 = 'initiator'
else:
configuration.zoning_policy_BRCD_FAB_1 = 'initiator-target'
configuration.zone_activate_BRCD_FAB_1 = True
configuration.zone_name_prefix_BRCD_FAB_1 = 'openstack_fab1'
configuration.principal_switch_wwn_BRCD_FAB_1 = '100000051e55a100'
return configuration
class TestBrcdFcZoneDriver(BrcdFcZoneDriverBaseTest, test.TestCase):
def setUp(self):
super(TestBrcdFcZoneDriver, self).setUp()
# setup config for normal flow
self.setup_driver(self.setup_config(True, 1))
GlobalVars._zone_state = []
def setup_driver(self, config):
self.driver = importutils.import_object(
'cinder.zonemanager.drivers.brocade.brcd_fc_zone_driver'
'.BrcdFCZoneDriver', configuration=config)
def fake_get_active_zone_set(self, fabric_ip, fabric_user, fabric_pwd):
return GlobalVars._active_cfg
def fake_get_san_context(self, target_wwn_list):
fabric_map = {}
return fabric_map
def test_add_connection(self):
"""Normal flow for i-t mode."""
GlobalVars._active_cfg = _active_cfg_before_add
GlobalVars._is_normal_test = True
GlobalVars._zone_state = []
LOG.info(_("In Add GlobalVars._active_cfg:"
" %s"), GlobalVars._active_cfg)
LOG.info(_("In Add GlobalVars._is_normal_test: "
"%s"), GlobalVars._is_normal_test)
LOG.info(_("In Add GlobalVars._zone_state:"
" %s"), GlobalVars._zone_state)
self.driver.add_connection('BRCD_FAB_1', _initiator_target_map)
self.assertTrue(_zone_name in GlobalVars._zone_state)
def test_delete_connection(self):
GlobalVars._is_normal_test = True
GlobalVars._active_cfg = _active_cfg_before_delete
self.driver.delete_connection(
'BRCD_FAB_1', _initiator_target_map)
self.assertFalse(_zone_name in GlobalVars._zone_state)
def test_add_connection_for_initiator_mode(self):
"""Normal flow for i mode."""
GlobalVars._is_normal_test = True
GlobalVars._active_cfg = _active_cfg_before_add
self.setup_driver(self.setup_config(True, 2))
self.driver.add_connection('BRCD_FAB_1', _initiator_target_map)
self.assertTrue(_zone_name in GlobalVars._zone_state)
def test_delete_connection_for_initiator_mode(self):
GlobalVars._is_normal_test = True
GlobalVars._active_cfg = _active_cfg_before_delete
self.setup_driver(self.setup_config(True, 2))
self.driver.delete_connection(
'BRCD_FAB_1', _initiator_target_map)
self.assertFalse(_zone_name in GlobalVars._zone_state)
def test_add_connection_for_invalid_fabric(self):
"""Test abnormal flows."""
GlobalVars._is_normal_test = True
GlobalVars._active_cfg = _active_cfg_before_add
GlobalVars._is_normal_test = False
self.setup_driver(self.setup_config(False, 1))
self.assertRaises(exception.FCZoneDriverException,
self.driver.add_connection,
'BRCD_FAB_1',
_initiator_target_map)
def test_delete_connection_for_invalid_fabric(self):
GlobalVars._active_cfg = _active_cfg_before_delete
GlobalVars._is_normal_test = False
self.setup_driver(self.setup_config(False, 1))
self.assertRaises(exception.FCZoneDriverException,
self.driver.delete_connection,
'BRCD_FAB_1',
_initiator_target_map)
class FakeBrcdFCZoneClientCLI(object):
def __init__(self, ipaddress, username, password, port):
LOG.info(_("User: %s"), username)
LOG.info(_("_zone_state: %s"), GlobalVars._zone_state)
if not GlobalVars._is_normal_test:
raise paramiko.SSHException("Unable to connect to fabric")
def get_active_zone_set(self):
LOG.debug(_("Inside get_active_zone_set %s"), GlobalVars._active_cfg)
return GlobalVars._active_cfg
def add_zones(self, zones, isActivate):
GlobalVars._zone_state.extend(zones.keys())
def delete_zones(self, zone_names, isActivate):
zone_list = zone_names.split(';')
GlobalVars._zone_state = [
x for x in GlobalVars._zone_state if x not in zone_list]
def is_supported_firmware(self):
return True
def get_nameserver_info(self):
return _target_ns_map
def close_connection(self):
pass
def cleanup(self):
pass
class FakeBrcdFCSanLookupService(object):
def get_device_mapping_from_network(self,
initiator_wwn_list,
target_wwn_list):
device_map = {}
initiators = []
targets = []
for i in initiator_wwn_list:
if (i in _initiator_ns_map[_fabric_wwn]):
initiators.append(i)
for t in target_wwn_list:
if (t in _target_ns_map[_fabric_wwn]):
targets.append(t)
device_map[_fabric_wwn] = {
'initiator_port_wwn_list': initiators,
'target_port_wwn_list': targets}
return device_map
class GlobalVars(object):
global _active_cfg
_active_cfg = {}
global _zone_state
_zone_state = list()
global _is_normal_test
_is_normal_test = True

View File

@ -0,0 +1,100 @@
# (c) Copyright 2013 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Unit tests for fc san lookup service."""
from cinder import exception
from cinder.openstack.common import log as logging
from cinder import test
from cinder.volume import configuration as conf
from cinder.zonemanager.fc_san_lookup_service import FCSanLookupService
LOG = logging.getLogger(__name__)
_target_ns_map = {'100000051e55a100': ['20240002ac000a50']}
_initiator_ns_map = {'100000051e55a100': ['10008c7cff523b01']}
_device_map_to_verify = {
'100000051e55a100': {
'initiator_port_wwn_list': [
'10008c7cff523b01'], 'target_port_wwn_list': ['20240002ac000a50']}}
_fabric_wwn = '100000051e55a100'
class TestFCSanLookupService(FCSanLookupService, test.TestCase):
def setUp(self):
super(TestFCSanLookupService, self).setUp()
self.configuration = self.setup_config()
# override some of the functions
def __init__(self, *args, **kwargs):
test.TestCase.__init__(self, *args, **kwargs)
def setup_config(self):
configuration = conf.Configuration(None)
# fill up config
configuration.fc_san_lookup_service = (
'cinder.tests.test_brcd_lookup_service.FakeBrcdFCSanLookupService')
return configuration
def test_get_device_mapping_from_network(self):
GlobalParams._is_normal_test = True
initiator_list = ['10008c7cff523b01']
target_list = ['20240002ac000a50', '20240002ac000a40']
device_map = self.get_device_mapping_from_network(
initiator_list, target_list)
self.assertDictMatch(device_map, _device_map_to_verify)
def test_get_device_mapping_from_network_for_invalid_config(self):
GlobalParams._is_normal_test = False
initiator_list = ['10008c7cff523b01']
target_list = ['20240002ac000a50', '20240002ac000a40']
self.assertRaises(exception.FCSanLookupServiceException,
self.get_device_mapping_from_network,
initiator_list, target_list)
class FakeBrcdFCSanLookupService(object):
def __init__(self, **kwargs):
pass
def get_device_mapping_from_network(self,
initiator_wwn_list,
target_wwn_list):
if not GlobalParams._is_normal_test:
raise exception.FCSanLookupServiceException("Error")
device_map = {}
initiators = []
targets = []
for i in initiator_wwn_list:
if (i in _initiator_ns_map[_fabric_wwn]):
initiators.append(i)
for t in target_wwn_list:
if (t in _target_ns_map[_fabric_wwn]):
targets.append(t)
device_map[_fabric_wwn] = {
'initiator_port_wwn_list': initiators,
'target_port_wwn_list': targets}
return device_map
class GlobalParams(object):
global _is_normal_test
_is_normal_test = True

View File

@ -0,0 +1,78 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Unit tests for FC Zone Manager."""
import mock
from cinder import exception
from cinder import test
from cinder.volume import configuration as conf
from cinder.zonemanager.drivers.fc_zone_driver import FCZoneDriver
from cinder.zonemanager.fc_zone_manager import ZoneManager
from mock import Mock
fabric_name = 'BRCD_FAB_3'
init_target_map = {'10008c7cff523b01': ['20240002ac000a50']}
fabric_map = {'BRCD_FAB_3': ['20240002ac000a50']}
target_list = ['20240002ac000a50']
class TestFCZoneManager(ZoneManager, test.TestCase):
def setUp(self):
super(TestFCZoneManager, self).setUp()
self.configuration = conf.Configuration(None)
self.configuration.set_default('fc_fabric_names', fabric_name)
self.driver = Mock(FCZoneDriver)
def __init__(self, *args, **kwargs):
test.TestCase.__init__(self, *args, **kwargs)
def test_add_connection(self):
with mock.patch.object(self.driver, 'add_connection')\
as add_connection_mock:
self.driver.get_san_context.return_value = fabric_map
self.add_connection(init_target_map)
self.driver.get_san_context.assert_called_once(target_list)
add_connection_mock.assert_called_once_with(fabric_name,
init_target_map)
def test_add_connection_error(self):
with mock.patch.object(self.driver, 'add_connection')\
as add_connection_mock:
add_connection_mock.side_effect = exception.FCZoneDriverException
self.assertRaises(exception.ZoneManagerException,
self.add_connection, init_target_map)
def test_delete_connection(self):
with mock.patch.object(self.driver, 'delete_connection')\
as delete_connection_mock:
self.driver.get_san_context.return_value = fabric_map
self.delete_connection(init_target_map)
self.driver.get_san_context.assert_called_once_with(target_list)
delete_connection_mock.assert_called_once_with(fabric_name,
init_target_map)
def test_delete_connection_error(self):
with mock.patch.object(self.driver, 'delete_connection')\
as del_connection_mock:
del_connection_mock.side_effect = exception.FCZoneDriverException
self.assertRaises(exception.ZoneManagerException,
self.delete_connection, init_target_map)

View File

@ -0,0 +1,148 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Unit tests for Volume Manager."""
import mock
from cinder import exception
from cinder import test
from cinder import utils
from cinder.volume import configuration as conf
from cinder.volume.driver import VolumeDriver
from cinder.volume.manager import VolumeManager
from cinder.zonemanager.fc_zone_manager import ZoneManager
from mock import Mock
from mock import patch
init_target_map = {'10008c7cff523b01': ['20240002ac000a50']}
conn_info = {
'driver_volume_type': 'fibre_channel',
'data': {
'target_discovered': True,
'target_lun': 1,
'target_wwn': '20240002ac000a50',
'initiator_target_map': {
'10008c7cff523b01': ['20240002ac000a50']
}
}
}
conn_info_no_init_target_map = {
'driver_volume_type': 'fibre_channel',
'data': {
'target_discovered': True,
'target_lun': 1,
'target_wwn': '20240002ac000a50',
}
}
class TestVolumeManager(VolumeManager, test.TestCase):
def setUp(self):
super(TestVolumeManager, self).setUp()
self.configuration = conf.Configuration(None)
self.configuration.set_default('fc_fabric_names', 'BRCD_FAB_4')
self.configuration.zoning_mode = 'fabric'
self.driver = Mock(VolumeDriver)
self.driver.initialize_connection.return_value = conn_info
self.driver.terminate_connection.return_value = conn_info
self.driver.create_export.return_value = None
self.db = Mock()
self.db.volume_get.return_value = {'volume_type_id': None}
self.db.volume_admin_metadata_get.return_value = {}
self.context_mock = Mock()
self.context_mock.elevated.return_value = None
def __init__(self, *args, **kwargs):
test.TestCase.__init__(self, *args, **kwargs)
@patch.object(utils, 'require_driver_initialized')
def test_initialize_connection_voltype_fc_mode_fabric(self,
utils_mock):
utils_mock.return_value = True
with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\
as add_del_conn_mock:
self.initialize_connection(self.context_mock, None, None)
add_del_conn_mock.assert_called_once_with(conn_info, 1)
@patch.object(utils, 'require_driver_initialized')
def test_initialize_connection_voltype_fc_mode_none(self,
utils_mock):
utils_mock.return_value = True
with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\
as add_del_conn_mock:
self.configuration.zoning_mode = 'none'
self.initialize_connection(self.context_mock, None, None)
assert not add_del_conn_mock.called
def test_terminate_connection_exception(self):
with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\
as add_del_conn_mock:
add_del_conn_mock.side_effect = exception.ZoneManagerException
self.assertRaises(exception.VolumeBackendAPIException,
self.terminate_connection, None, None, None,
False)
@patch.object(utils, 'require_driver_initialized')
def test_terminate_connection_voltype_fc_mode_fabric(self,
utils_mock):
utils_mock.return_value = True
with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\
as add_del_conn_mock:
self.terminate_connection(None, None, None, False)
add_del_conn_mock.assert_called_once_with(conn_info, 0)
@patch.object(utils, 'require_driver_initialized')
def test_terminate_connection_mode_none(self,
utils_mock):
utils_mock.return_value = True
with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\
as add_del_conn_mock:
self.configuration.zoning_mode = 'none'
self.terminate_connection(None, None, None, False)
assert not add_del_conn_mock.called
@patch.object(utils, 'require_driver_initialized')
def test_terminate_connection_conn_info_none(self,
utils_mock):
utils_mock.return_value = True
self.driver.terminate_connection.return_value = None
with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\
as add_del_conn_mock:
self.terminate_connection(None, None, None, False)
assert not add_del_conn_mock.called
@patch.object(ZoneManager, 'add_connection')
def test__add_or_delete_connection_add(self,
add_connection_mock):
self._add_or_delete_fc_connection(conn_info, 1)
add_connection_mock.assert_called_once_with(init_target_map)
@patch.object(ZoneManager, 'delete_connection')
def test__add_or_delete_connection_delete(self,
delete_connection_mock):
self._add_or_delete_fc_connection(conn_info, 0)
delete_connection_mock.assert_called_once_with(init_target_map)
@patch.object(ZoneManager, 'delete_connection')
def test__add_or_delete_connection_no_init_target_map(self,
del_conn_mock):
self._add_or_delete_fc_connection(conn_info_no_init_target_map, 0)
assert not del_conn_mock.called

View File

@ -58,6 +58,7 @@ from cinder.volume.flows.manager import create_volume
from cinder.volume import rpcapi as volume_rpcapi from cinder.volume import rpcapi as volume_rpcapi
from cinder.volume import utils as volume_utils from cinder.volume import utils as volume_utils
from cinder.volume import volume_types from cinder.volume import volume_types
from cinder.zonemanager.fc_zone_manager import ZoneManager
from eventlet.greenpool import GreenPool from eventlet.greenpool import GreenPool
@ -77,6 +78,9 @@ volume_manager_opts = [
default=False, default=False,
help='Offload pending volume delete during ' help='Offload pending volume delete during '
'volume service startup'), 'volume service startup'),
cfg.StrOpt('zoning_mode',
default='none',
help='FC Zoning mode configured'),
] ]
CONF = cfg.CONF CONF = cfg.CONF
@ -821,6 +825,13 @@ class VolumeManager(manager.SchedulerDependentManager):
if volume_metadata.get('readonly') == 'True' if volume_metadata.get('readonly') == 'True'
else 'rw') else 'rw')
conn_info['data']['access_mode'] = access_mode conn_info['data']['access_mode'] = access_mode
# NOTE(skolathur): If volume_type is fibre_channel, invoke
# FCZoneManager to add access control via FC zoning.
vol_type = conn_info.get('driver_volume_type', None)
mode = self.configuration.zoning_mode
LOG.debug(_("Zoning Mode: %s"), mode)
if vol_type == 'fibre_channel' and mode == 'fabric':
self._add_or_delete_fc_connection(conn_info, 1)
return conn_info return conn_info
def terminate_connection(self, context, volume_id, connector, force=False): def terminate_connection(self, context, volume_id, connector, force=False):
@ -835,8 +846,17 @@ class VolumeManager(manager.SchedulerDependentManager):
volume_ref = self.db.volume_get(context, volume_id) volume_ref = self.db.volume_get(context, volume_id)
try: try:
self.driver.terminate_connection(volume_ref, conn_info = self.driver.terminate_connection(volume_ref,
connector, force=force) connector,
force=force)
# NOTE(skolathur): If volume_type is fibre_channel, invoke
# FCZoneManager to remove access control via FC zoning.
if conn_info:
vol_type = conn_info.get('driver_volume_type', None)
mode = self.configuration.zoning_mode
LOG.debug(_("Zoning Mode: %s"), mode)
if vol_type == 'fibre_channel' and mode == 'fabric':
self._add_or_delete_fc_connection(conn_info, 0)
except Exception as err: except Exception as err:
err_msg = (_('Unable to terminate volume connection: %(err)s') err_msg = (_('Unable to terminate volume connection: %(err)s')
% {'err': str(err)}) % {'err': str(err)})
@ -1246,3 +1266,37 @@ class VolumeManager(manager.SchedulerDependentManager):
if new_reservations: if new_reservations:
QUOTAS.commit(context, new_reservations, project_id=project_id) QUOTAS.commit(context, new_reservations, project_id=project_id)
self.publish_service_capabilities(context) self.publish_service_capabilities(context)
def _add_or_delete_fc_connection(self, conn_info, zone_op):
"""Add or delete connection control to fibre channel network.
In case of fibre channel, when zoning mode is set as fabric
ZoneManager is invoked to apply FC zoning configuration to the network
using initiator and target WWNs used for attach/detach.
params conn_info: connector passed by volume driver after
initialize_connection or terminate_connection.
params zone_op: Indicates if it is a zone add or delete operation
zone_op=0 for delete connection and 1 for add connection
"""
_initiator_target_map = None
if 'initiator_target_map' in conn_info['data']:
_initiator_target_map = conn_info['data']['initiator_target_map']
LOG.debug(_("Initiator Target map:%s"), _initiator_target_map)
# NOTE(skolathur): Invoke Zonemanager to handle automated FC zone
# management when vol_type is fibre_channel and zoning_mode is fabric
# Initiator_target map associating each initiator WWN to one or more
# target WWN is passed to ZoneManager to add or update zone config.
LOG.debug(_("Zoning op: %s"), zone_op)
if _initiator_target_map is not None:
kwargs = {'driver_volume_type': 'fibre_channel',
'configuration': self.configuration}
zonemanager = ZoneManager(**kwargs)
try:
if zone_op == 1:
zonemanager.add_connection(_initiator_target_map)
elif zone_op == 0:
zonemanager.delete_connection(_initiator_target_map)
except exception.ZoneManagerException as e:
with excutils.save_and_reraise_exception():
LOG.error(str(e))

View File

@ -0,0 +1,27 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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.
#
"""
:mod:`cinder.zonemanager` -- FC Zone manager
=====================================================
.. automodule:: cinder.zonemanager
:platform: Unix
:synopsis: Module containing all the FC Zone Manager classes
"""

View File

@ -0,0 +1,27 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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.
#
"""
:mod:`cinder.zonemanager.driver` -- FC Zone Drivers
=====================================================
.. automodule:: cinder.zonemanager.driver
:platform: Unix
:synopsis: Module containing all the FC Zone drivers.
"""

View File

@ -0,0 +1,27 @@
# (c) Copyright 2013 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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.
#
"""
:mod:`cinder.zonemanager.driver.brocade` -- Brocade FC Zone Drivers
=====================================================
.. automodule:: cinder.zonemanager.driver.brocade
:platform: Unix
:synopsis: Module containing all the Brocade FC Zone drivers.
"""

View File

@ -0,0 +1,276 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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 paramiko
from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import excutils
from cinder.openstack.common import log as logging
from cinder import utils
import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant
from cinder.zonemanager.drivers.fc_common import FCCommon
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.import_opt('fc_fabric_names', 'cinder.zonemanager.drivers.fc_common')
class BrcdFCSanLookupService(FCCommon):
def __init__(self, **kwargs):
"""Initializing the client."""
super(BrcdFCSanLookupService, self).__init__(**kwargs)
self.configuration = kwargs.get('configuration', None)
self.create_configuration()
self.client = paramiko.SSHClient()
self.client.load_system_host_keys()
self.client.set_missing_host_key_policy(paramiko.WarningPolicy())
def create_configuration(self):
"""Configuration specific to SAN context values."""
config = self.configuration
fc_fabric_opts = []
fabric_names = config.fc_fabric_names.split(',')
LOG.debug(_('Fabric Names: %s'), fabric_names)
# There can be more than one SAN in the network and we need to
# get credentials for each for SAN context lookup later.
if len(fabric_names) > 0:
for fabric_name in fabric_names:
fc_fabric_opts.append(cfg.StrOpt('fc_fabric_address_'
+ fabric_name,
default='',
help='Management IP '
'of fabric'))
fc_fabric_opts.append(cfg.StrOpt('fc_fabric_user_'
+ fabric_name,
default='',
help='Fabric user ID'))
fc_fabric_opts.append(cfg.StrOpt('fc_fabric_password_'
+ fabric_name,
default='',
help='Password for user',
secret=True))
fc_fabric_opts.append(cfg.IntOpt('fc_fabric_port_'
+ fabric_name, default=22,
help='Connecting port'))
fc_fabric_opts.append(cfg.StrOpt('principal_switch_wwn_'
+ fabric_name,
default=fabric_name,
help='Principal switch WWN '
'of the fabric'))
config.append_config_values(fc_fabric_opts)
def get_device_mapping_from_network(self,
initiator_wwn_list,
target_wwn_list):
"""Provides the initiator/target map for available SAN contexts.
Looks up nameserver of each fc SAN configured to find logged in devices
and returns a map of initiator and target port WWNs for each fabric.
:param initiator_wwn_list: List of initiator port WWN
:param target_wwn_list: List of target port WWN
:returns List -- device wwn map in following format
{
<San name>: {
'initiator_port_wwn_list':
('200000051e55a100', '200000051e55a121'..)
'target_port_wwn_list':
('100000051e55a100', '100000051e55a121'..)
}
}
:raises Exception when connection to fabric is failed
"""
device_map = {}
formatted_target_list = []
formatted_initiator_list = []
fabric_map = {}
fabric_names = self.configuration.fc_fabric_names
fabrics = None
if not fabric_names:
raise exception.InvalidParameterValue(
err=_("Missing Fibre Channel SAN configuration "
"param - fc_fabric_names"))
fabrics = fabric_names.split(',')
LOG.debug(_("FC Fabric List: %s"), fabrics)
if fabrics:
for t in target_wwn_list:
formatted_target_list.append(self.get_formatted_wwn(t))
for i in initiator_wwn_list:
formatted_initiator_list.append(self.
get_formatted_wwn(i))
for fabric_name in fabrics:
fabric_ip = self.configuration.safe_get('fc_fabric_address_'
+ fabric_name)
fabric_user = self.configuration.safe_get('fc_fabric_user_'
+ fabric_name)
fabric_pwd = self.configuration.safe_get('fc_fabric_password_'
+ fabric_name)
fabric_port = self.configuration.safe_get(
'fc_fabric_port_' + fabric_name)
fabric_principal_wwn = self.configuration.safe_get(
'principal_switch_wwn_'
+ fabric_name)
# Get name server data from fabric and find the targets
# logged in
nsinfo = ''
try:
LOG.debug(_("Getting name server data for "
"fabric %s"), fabric_ip)
self.client.connect(
fabric_ip, fabric_port, fabric_user, fabric_pwd)
nsinfo = self.get_nameserver_info()
except exception.FCSanLookupServiceException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed collecting name server info from "
"fabric %s") % fabric_ip)
except Exception as e:
msg = _("SSH connection failed "
"for %(fabric) with error: %(err)"
) % {'fabric': fabric_ip, 'err': str(e)}
LOG.error(msg)
raise exception.FCSanLookupServiceException(message=msg)
finally:
self.close_connection()
LOG.debug(_("Lookup service:nsinfo-%s"), nsinfo)
LOG.debug(_("Lookup service:initiator list from "
"caller-%s"), formatted_initiator_list)
LOG.debug(_("Lookup service:target list from "
"caller-%s"), formatted_target_list)
visible_targets = filter(lambda x: x in formatted_target_list,
nsinfo)
visible_initiators = filter(lambda x: x in
formatted_initiator_list, nsinfo)
if visible_targets:
LOG.debug(_("Filtered targets is: %s"), visible_targets)
# getting rid of the : before returning
for idx, elem in enumerate(visible_targets):
elem = str(elem).replace(':', '')
visible_targets[idx] = elem
else:
LOG.debug(_("No targets are in the nameserver for SAN %s"),
fabric_name)
if visible_initiators:
# getting rid of the : before returning ~sk
for idx, elem in enumerate(visible_initiators):
elem = str(elem).replace(':', '')
visible_initiators[idx] = elem
else:
LOG.debug(_("No initiators are in the nameserver "
"for SAN %s"), fabric_name)
fabric_map = {
'initiator_port_wwn_list': visible_initiators,
'target_port_wwn_list': visible_targets
}
device_map[fabric_principal_wwn] = fabric_map
LOG.debug(_("Device map for SAN context: %s"), device_map)
return device_map
def get_nameserver_info(self):
"""Get name server data from fabric.
This method will return the connected node port wwn list(local
and remote) for the given switch fabric
"""
cli_output = None
nsinfo_list = []
try:
cli_output = self._get_switch_data(ZoneConstant.NS_SHOW)
except exception.FCSanLookupServiceException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed collecting nsshow info for fabric"))
if cli_output:
nsinfo_list = self._parse_ns_output(cli_output)
try:
cli_output = self._get_switch_data(ZoneConstant.NS_CAM_SHOW)
except exception.FCSanLookupServiceException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed collecting nscamshow"))
if cli_output:
nsinfo_list.extend(self._parse_ns_output(cli_output))
LOG.debug(_("Connector returning nsinfo-%s"), nsinfo_list)
return nsinfo_list
def close_connection(self):
"""This will close the client connection."""
self.client.close()
self.client = None
def _get_switch_data(self, cmd):
stdin, stdout, stderr = None, None, None
utils.check_ssh_injection([cmd])
try:
stdin, stdout, stderr = self.client.exec_command(cmd)
switch_data = stdout.readlines()
except paramiko.SSHException as e:
msg = (_("SSH Command failed with error '%(err)r' "
"'%(command)s'") % {'err': str(e), 'command': cmd})
LOG.error(msg)
raise exception.FCSanLookupServiceException(message=msg)
finally:
if (stdin):
stdin.flush()
stdin.close()
if (stdout):
stdout.close()
if (stderr):
stderr.close()
return switch_data
def _parse_ns_output(self, switch_data):
"""Parses name server data.
Parses nameserver raw data and adds the device port wwns to the list
:returns list of device port wwn from ns info
"""
nsinfo_list = []
for line in switch_data:
if not(" NL " in line or " N " in line):
continue
linesplit = line.split(';')
if len(linesplit) > 2:
node_port_wwn = linesplit[2]
nsinfo_list.append(node_port_wwn)
else:
msg = _("Malformed nameserver string: %s") % line
LOG.error(msg)
raise exception.InvalidParameterValue(err=msg)
return nsinfo_list
def get_formatted_wwn(self, wwn_str):
"""Utility API that formats WWN to insert ':'."""
if (len(wwn_str) != 16):
return wwn_str.lower()
else:
return (':'.join([wwn_str[i:i + 2]
for i in range(0, len(wwn_str), 2)])).lower()

View File

@ -0,0 +1,543 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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.
#
"""
Script to push the zone configuration to brocade SAN switches.
"""
import random
import re
from eventlet import greenthread
from cinder import exception
from cinder.openstack.common import excutils
from cinder.openstack.common import log as logging
from cinder.openstack.common import processutils
from cinder import utils
import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant
LOG = logging.getLogger(__name__)
class BrcdFCZoneClientCLI(object):
switch_ip = None
switch_port = '22'
switch_user = 'admin'
switch_pwd = 'none'
patrn = re.compile('[;\s]+')
def __init__(self, ipaddress, username, password, port):
"""initializing the client."""
self.switch_ip = ipaddress
self.switch_port = port
self.switch_user = username
self.switch_pwd = password
self.sshpool = None
def get_active_zone_set(self):
"""Return the active zone configuration.
Return active zoneset from fabric. When none of the configurations
are active then it will return empty map.
:returns: Map -- active zone set map in the following format
{
'zones':
{'openstack50060b0000c26604201900051ee8e329':
['50060b0000c26604', '201900051ee8e329']
},
'active_zone_config': 'OpenStack_Cfg'
}
"""
zone_set = {}
zone = {}
zone_member = None
zone_name = None
switch_data = None
zone_set_name = None
try:
switch_data = self._get_switch_info(
[ZoneConstant.GET_ACTIVE_ZONE_CFG])
except exception.BrocadeZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed getting active zone set "
"from fabric %s"), self.switch_ip)
try:
for line in switch_data:
line_split = re.split('\\t', line)
if len(line_split) > 2:
line_split = [x.replace(
'\n', '') for x in line_split]
line_split = [x.replace(
' ',
'') for x in line_split]
if ZoneConstant.CFG_ZONESET in line_split:
zone_set_name = line_split[1]
continue
if line_split[1]:
zone_name = line_split[1]
zone[zone_name] = list()
if line_split[2]:
zone_member = line_split[2]
if zone_member:
zone_member_list = zone.get(zone_name)
zone_member_list.append(zone_member)
zone_set[ZoneConstant.CFG_ZONES] = zone
zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG] = zone_set_name
except Exception as ex:
# Incase of parsing error here, it should be malformed cli output.
msg = _("Malformed zone configuration: (switch=%(switch)s "
"zone_config=%(zone_config)s)."
) % {'switch': self.switch_ip,
'zone_config': switch_data}
LOG.error(msg)
LOG.exception(ex)
raise exception.FCZoneDriverException(reason=msg)
switch_data = None
return zone_set
def add_zones(self, zones, activate):
"""Add zone configuration.
This method will add the zone configuration passed by user.
input params:
zones - zone names mapped to members.
zone members are colon separated but case-insensitive
{ zonename1:[zonememeber1,zonemember2,...],
zonename2:[zonemember1, zonemember2,...]...}
e.g: {'openstack50060b0000c26604201900051ee8e329':
['50:06:0b:00:00:c2:66:04', '20:19:00:05:1e:e8:e3:29']
}
activate - True/False
"""
LOG.debug(_("Add Zones - Zones passed: %s"), zones)
cfg_name = None
iterator_count = 0
zone_with_sep = ''
active_zone_set = self.get_active_zone_set()
LOG.debug(_("Active zone set:%s"), active_zone_set)
zone_list = active_zone_set[ZoneConstant.CFG_ZONES]
LOG.debug(_("zone list:%s"), zone_list)
for zone in zones.keys():
# if zone exists, its an update. Delete & insert
# TODO(skolathur): This can be optimized to an update call later
LOG.debug("Update call")
if (zone in zone_list):
try:
self.delete_zones(zone, activate)
except exception.BrocadeZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Deleting zone failed %s"), zone)
LOG.debug(_("Deleted Zone before insert : %s"), zone)
zone_members_with_sep = ';'.join(str(member) for
member in zones[zone])
LOG.debug(_("Forming command for add zone"))
cmd = 'zonecreate "%(zone)s", "%(zone_members_with_sep)s"' % {
'zone': zone,
'zone_members_with_sep': zone_members_with_sep}
LOG.debug(_("Adding zone, cmd to run %s"), cmd)
self.apply_zone_change(cmd.split())
LOG.debug(_("Created zones on the switch"))
if(iterator_count > 0):
zone_with_sep += ';'
iterator_count += 1
zone_with_sep += zone
try:
cfg_name = active_zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG]
cmd = None
if not cfg_name:
cfg_name = ZoneConstant.OPENSTACK_CFG_NAME
cmd = 'cfgcreate "%(zoneset)s", "%(zones)s"' \
% {'zoneset': cfg_name, 'zones': zone_with_sep}
else:
cmd = 'cfgadd "%(zoneset)s", "%(zones)s"' \
% {'zoneset': cfg_name, 'zones': zone_with_sep}
LOG.debug(_("New zone %s"), cmd)
self.apply_zone_change(cmd.split())
self._cfg_save()
if activate:
self.activate_zoneset(cfg_name)
except Exception as e:
self._cfg_trans_abort()
msg = _("Creating and activating zone set failed: "
"(Zone set=%(cfg_name)s error=%(err)s)."
) % {'cfg_name': cfg_name, 'err': str(e)}
LOG.error(msg)
raise exception.BrocadeZoningCliException(reason=msg)
def activate_zoneset(self, cfgname):
"""Method to Activate the zone config. Param cfgname - ZonesetName."""
cmd_list = [ZoneConstant.ACTIVATE_ZONESET, cfgname]
return self._ssh_execute(cmd_list, True, 1)
def deactivate_zoneset(self):
"""Method to deActivate the zone config."""
return self._ssh_execute([ZoneConstant.DEACTIVATE_ZONESET], True, 1)
def delete_zones(self, zone_names, activate):
"""Delete zones from fabric.
Method to delete the active zone config zones
params zone_names: zoneNames separated by semicolon
params activate: True/False
"""
active_zoneset_name = None
active_zone_set = None
zone_list = []
active_zone_set = self.get_active_zone_set()
active_zoneset_name = active_zone_set[
ZoneConstant.ACTIVE_ZONE_CONFIG]
zone_list = active_zone_set[ZoneConstant.CFG_ZONES]
zones = self.patrn.split(''.join(zone_names))
cmd = None
try:
if len(zones) == len(zone_list):
self.deactivate_zoneset()
cmd = 'cfgdelete "%(active_zoneset_name)s"' \
% {'active_zoneset_name': active_zoneset_name}
# Active zoneset is being deleted, hence reset is_active
activate = False
else:
cmd = 'cfgremove "%(active_zoneset_name)s", "%(zone_names)s"' \
% {'active_zoneset_name': active_zoneset_name,
'zone_names': zone_names
}
LOG.debug(_("Delete zones: Config cmd to run:%s"), cmd)
self.apply_zone_change(cmd.split())
for zone in zones:
self._zone_delete(zone)
self._cfg_save()
if activate:
self.activate_zoneset(active_zoneset_name)
except Exception as e:
msg = _("Deleting zones failed: (command=%(cmd)s error=%(err)s)."
) % {'cmd': cmd, 'err': str(e)}
LOG.error(msg)
self._cfg_trans_abort()
raise exception.BrocadeZoningCliException(reason=msg)
def get_nameserver_info(self):
"""Get name server data from fabric.
This method will return the connected node port wwn list(local
and remote) for the given switch fabric
"""
cli_output = None
return_list = []
try:
cli_output = self._get_switch_info([ZoneConstant.NS_SHOW])
except exception.BrocadeZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed collecting nsshow "
"info for fabric %s"), self.switch_ip)
if (cli_output):
return_list = self._parse_ns_output(cli_output)
try:
cli_output = self._get_switch_info([ZoneConstant.NS_CAM_SHOW])
except exception.BrocadeZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed collecting nscamshow "
"info for fabric %s"), self.switch_ip)
if (cli_output):
return_list.extend(self._parse_ns_output(cli_output))
cli_output = None
return return_list
def _cfg_save(self):
self._ssh_execute([ZoneConstant.CFG_SAVE], True, 1)
def _zone_delete(self, zone_name):
cmd = 'zonedelete "%(zone_name)s"' % {'zone_name': zone_name}
self.apply_zone_change(cmd.split())
def _cfg_trans_abort(self):
is_abortable = self._is_trans_abortable()
if(is_abortable):
self.apply_zone_change([ZoneConstant.CFG_ZONE_TRANS_ABORT])
def _is_trans_abortable(self):
is_abortable = False
stdout, stderr = None, None
stdout, stderr = self._run_ssh(
[ZoneConstant.CFG_SHOW_TRANS], True, 1)
output = stdout.splitlines()
is_abortable = False
for line in output:
if(ZoneConstant.TRANS_ABORTABLE in line):
is_abortable = True
break
if stderr:
msg = _("Error while checking transaction status: %s") % stderr
raise exception.BrocadeZoningCliException(reason=msg)
else:
return is_abortable
def apply_zone_change(self, cmd_list):
"""Execute zoning cli with no status update.
Executes CLI commands such as addZone where status return is
not expected.
"""
stdout, stderr = None, None
LOG.debug(_("Executing command via ssh: %s"), cmd_list)
stdout, stderr = self._run_ssh(cmd_list, True, 1)
# no output expected, so output means there is an error
if stdout:
msg = _("Error while running zoning CLI: (command=%(cmd)s "
"error=%(err)s).") % {'cmd': cmd_list, 'err': stdout}
LOG.error(msg)
self._cfg_trans_abort()
raise exception.BrocadeZoningCliException(reason=msg)
def is_supported_firmware(self):
"""Check firmware version is v6.4 or higher.
This API checks if the firmware version per the plug-in support level.
This only checks major and minor version.
"""
cmd = ['version']
firmware = 0
try:
stdout, stderr = self._execute_shell_cmd(cmd)
if (stdout):
for line in stdout:
if 'Fabric OS: v' in line:
LOG.debug(_("Firmware version string:%s"), line)
ver = line.split('Fabric OS: v')[1].split('.')
if (ver):
firmware = int(ver[0] + ver[1])
return firmware > 63
else:
LOG.error(_("No CLI output for firmware version check"))
return False
except processutils.ProcessExecutionError as e:
msg = _("Error while getting data via ssh: (command=%(cmd)s "
"error=%(err)s).") % {'cmd': cmd, 'err': str(e)}
LOG.error(msg)
raise exception.BrocadeZoningCliException(reason=msg)
def _get_switch_info(self, cmd_list):
stdout, stderr, sw_data = None, None, None
try:
stdout, stderr = self._run_ssh(cmd_list, True, 1)
if (stdout):
sw_data = stdout.splitlines()
return sw_data
except processutils.ProcessExecutionError as e:
msg = _("Error while getting data via ssh: (command=%(cmd)s "
"error=%(err)s).") % {'cmd': cmd_list, 'err': str(e)}
LOG.error(msg)
raise exception.BrocadeZoningCliException(reason=msg)
def _parse_ns_output(self, switch_data):
"""Parses name server data.
Parses nameserver raw data and adds the device port wwns to the list
:returns: List -- list of device port wwn from ns info
"""
return_list = []
for line in switch_data:
if not(" NL " in line or " N " in line):
continue
linesplit = line.split(';')
if len(linesplit) > 2:
node_port_wwn = linesplit[2]
return_list.append(node_port_wwn)
else:
msg = _("Malformed nameserver string: %s") % line
LOG.error(msg)
raise exception.InvalidParameterValue(err=msg)
return return_list
def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1):
# TODO(skolathur): Need to implement ssh_injection check
# currently, the check will fail for zonecreate command
# as zone members are separated by ';'which is a danger char
command = ' '. join(cmd_list)
if not self.sshpool:
self.sshpool = utils.SSHPool(self.switch_ip,
self.switch_port,
None,
self.switch_user,
self.switch_pwd,
min_size=1,
max_size=5)
last_exception = None
try:
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
return processutils.ssh_execute(
ssh,
command,
check_exit_code=check_exit_code)
except Exception as e:
LOG.error(e)
last_exception = e
greenthread.sleep(random.randint(20, 500) / 100.0)
try:
raise processutils.ProcessExecutionError(
exit_code=last_exception.exit_code,
stdout=last_exception.stdout,
stderr=last_exception.stderr,
cmd=last_exception.cmd)
except AttributeError:
raise processutils.ProcessExecutionError(
exit_code=-1,
stdout="",
stderr="Error running SSH command",
cmd=command)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_("Error running SSH command: %s") % command)
def _ssh_execute(self, cmd_list, check_exit_code=True, attempts=1):
"""Execute cli with status update.
Executes CLI commands such as cfgsave where status return is expected.
"""
utils.check_ssh_injection(cmd_list)
command = ' '. join(cmd_list)
if not self.sshpool:
self.sshpool = utils.SSHPool(self.switch_ip,
self.switch_port,
None,
self.switch_user,
self.switch_pwd,
min_size=1,
max_size=5)
stdin, stdout, stderr = None, None, None
LOG.debug(_("Executing command via ssh: %s") % command)
last_exception = None
try:
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
stdin, stdout, stderr = ssh.exec_command(command)
greenthread.sleep(random.randint(20, 500) / 100.0)
stdin.write("%s\n" % ZoneConstant.YES)
channel = stdout.channel
exit_status = channel.recv_exit_status()
LOG.debug(_("Exit Status from ssh:%s"), exit_status)
# exit_status == -1 if no exit code was returned
if exit_status != -1:
LOG.debug(_('Result was %s') % exit_status)
if check_exit_code and exit_status != 0:
raise processutils.ProcessExecutionError(
exit_code=exit_status,
stdout=stdout,
stderr=stderr,
cmd=command)
else:
return True
else:
return True
except Exception as e:
LOG.error(e)
last_exception = e
greenthread.sleep(random.randint(20, 500) / 100.0)
LOG.debug(_("Handling error case after "
"SSH:%s"), str(last_exception))
try:
raise processutils.ProcessExecutionError(
exit_code=last_exception.exit_code,
stdout=last_exception.stdout,
stderr=last_exception.stderr,
cmd=last_exception.cmd)
except AttributeError:
raise processutils.ProcessExecutionError(
exit_code=-1,
stdout="",
stderr="Error running SSH command",
cmd=command)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(_("Error executing command via ssh: %s"), str(e))
finally:
if stdin:
stdin.flush()
stdin.close()
if stdout:
stdout.close()
if stderr:
stderr.close()
def _execute_shell_cmd(self, cmd):
"""Run command over shell for older firmware versions.
We invoke shell and issue the command and return the output.
This is primarily used for issuing read commands when we are not sure
if the firmware supports exec_command.
"""
utils.check_ssh_injection(cmd)
command = ' '. join(cmd)
stdout, stderr = None, None
if not self.sshpool:
self.sshpool = utils.SSHPool(self.switch_ip,
self.switch_port,
None,
self.switch_user,
self.switch_pwd,
min_size=1,
max_size=5)
with self.sshpool.item() as ssh:
LOG.debug('Running cmd (SSH): %s' % command)
channel = ssh.invoke_shell()
stdin_stream = channel.makefile('wb')
stdout_stream = channel.makefile('rb')
stderr_stream = channel.makefile('rb')
stdin_stream.write('''%s
exit
''' % command)
stdin_stream.flush()
stdout = stdout_stream.readlines()
stderr = stderr_stream.readlines()
stdin_stream.close()
stdout_stream.close()
stderr_stream.close()
exit_status = channel.recv_exit_status()
# exit_status == -1 if no exit code was returned
if exit_status != -1:
LOG.debug('Result was %s' % exit_status)
if exit_status != 0:
msg = "command %s failed" % command
LOG.debug(msg)
raise processutils.ProcessExecutionError(
exit_code=exit_status,
stdout=stdout,
stderr=stderr,
cmd=command)
try:
channel.close()
except Exception as e:
LOG.exception(e)
LOG.debug("_execute_cmd: stdout to return:%s" % stdout)
LOG.debug("_execute_cmd: stderr to return:%s" % stderr)
return (stdout, stderr)
def cleanup(self):
self.sshpool = None

View File

@ -0,0 +1,529 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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.
#
"""
Brocade Zone Driver is responsible to manage access control using FC zoning
for Brocade FC fabrics.
This is a concrete implementation of FCZoneDriver interface implementing
add_connection and delete_connection interfaces.
**Related Flags**
:zone_activate: Used by: class: 'FCZoneDriver'. Defaults to True
:zone_name_prefix: Used by: class: 'FCZoneDriver'. Defaults to 'openstack'
"""
from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import excutils
from cinder.openstack.common import importutils
from cinder.openstack.common import lockutils
from cinder.openstack.common import log as logging
from cinder.zonemanager.drivers.fc_zone_driver import FCZoneDriver
LOG = logging.getLogger(__name__)
brcd_opts = [
cfg.StrOpt('brcd_sb_connector',
default='cinder.zonemanager.drivers.brocade'
'.brcd_fc_zone_client_cli.BrcdFCZoneClientCLI',
help='Southbound connector for zoning operation'),
]
CONF = cfg.CONF
CONF.register_opts(brcd_opts)
CONF.import_opt('zone_activate', 'cinder.zonemanager.drivers.fc_zone_driver')
CONF.import_opt('zone_name_prefix',
'cinder.zonemanager.drivers.fc_zone_driver')
CONF.import_opt('fc_fabric_names', 'cinder.zonemanager.drivers.fc_common')
class BrcdFCZoneDriver(FCZoneDriver):
"""Brocade FC zone driver implementation.
OpenStack Fibre Channel zone driver to manage FC zoning in
Brocade SAN fabrics.
Version history:
1.0 - Initial Brocade FC zone driver
"""
def __init__(self, **kwargs):
super(BrcdFCZoneDriver, self).__init__(**kwargs)
self.configuration = kwargs.get('configuration', None)
if self.configuration:
self.configuration.append_config_values(brcd_opts)
# Adding a hack to hendle parameters from super classes
# in case configured with multi backend.
fabric_names = self.configuration.safe_get('fc_fabric_names')
activate = self.configuration.safe_get('zone_activate')
prefix = self.configuration.safe_get('zone_name_prefix')
base_san_opts = []
if not fabric_names:
base_san_opts.append(
cfg.StrOpt('fc_fabric_names', default=None,
help='Comma separated list of fibre channel '
'fabric names. This list of names is used to'
' retrieve other SAN credentials for connecting'
' to each SAN fabric'
))
if not activate:
base_san_opts.append(
cfg.BoolOpt('zone_activate',
default=True,
help='Indicates whether zone should '
'be activated or not'))
if not prefix:
base_san_opts.append(
cfg.StrOpt('zone_name_prefix',
default="openstack",
help="A prefix to be used when naming zone"))
if len(base_san_opts) > 0:
CONF.register_opts(base_san_opts)
self.configuration.append_config_values(base_san_opts)
fabric_names = self.configuration.fc_fabric_names.split(',')
fc_fabric_opts = []
# There can be more than one SAN in the network and we need to
# get credentials for each SAN.
if fabric_names:
for fabric_name in fabric_names:
fc_fabric_opts.append(cfg.StrOpt('fc_fabric_address_'
+ fabric_name,
default='',
help='Management IP'
' of fabric'))
fc_fabric_opts.append(cfg.StrOpt('fc_fabric_user_'
+ fabric_name,
default='',
help='Fabric user ID'))
fc_fabric_opts.append(cfg.StrOpt('fc_fabric_password_'
+ fabric_name,
default='',
help='Password for user',
secret=True))
fc_fabric_opts.append(cfg.IntOpt('fc_fabric_port_'
+ fabric_name,
default=22,
help='Connecting port'))
fc_fabric_opts.append(cfg.StrOpt('zoning_policy_'
+ fabric_name,
default=self.configuration
.zoning_policy,
help='overridden '
'zoning policy'))
fc_fabric_opts.append(cfg.BoolOpt('zone_activate_'
+ fabric_name,
default=self
.configuration
.zone_activate,
help='overridden zoning '
'activation state'))
fc_fabric_opts.append(cfg.StrOpt('zone_name_prefix_'
+ fabric_name,
default=self.configuration
.zone_name_prefix,
help='overridden zone '
'name prefix'))
fc_fabric_opts.append(cfg.StrOpt('principal_switch_wwn_'
+ fabric_name,
default=fabric_name,
help='Principal switch '
'WWN of the fabric'))
self.configuration.append_config_values(fc_fabric_opts)
def get_formatted_wwn(self, wwn_str):
"""Utility API that formats WWN to insert ':'."""
wwn_str = wwn_str.encode('ascii')
if len(wwn_str) != 16:
return wwn_str
else:
return ':'.join(
[wwn_str[i:i + 2] for i in range(0, len(wwn_str), 2)])
@lockutils.synchronized('brcd', 'fcfabric-', True)
def add_connection(self, fabric, initiator_target_map):
"""Concrete implementation of add_connection.
Based on zoning policy and state of each I-T pair, list of zone
members are created and pushed to the fabric to add zones. The
new zones created or zones updated are activated based on isActivate
flag set in cinder.conf returned by volume driver after attach
operation.
:param fabric: Fabric name from cinder.conf file
:param initiator_target_map: Mapping of initiator to list of targets
"""
LOG.debug(_("Add connection for Fabric:%s"), fabric)
LOG.info(_("BrcdFCZoneDriver - Add connection "
"for I-T map: %s"), initiator_target_map)
fabric_ip = self.configuration.safe_get(
'fc_fabric_address_' + fabric)
fabric_user = self.configuration.safe_get(
'fc_fabric_user_' + fabric)
fabric_pwd = self.configuration.safe_get(
'fc_fabric_password_' + fabric)
fabric_port = self.configuration.safe_get(
'fc_fabric_port_' + fabric)
zoning_policy = self.configuration.zoning_policy
zoning_policy_fab = self.configuration.safe_get(
'zoning_policy_' + fabric)
if zoning_policy_fab:
zoning_policy = zoning_policy_fab
LOG.info(_("Zoning policy for Fabric %s"), zoning_policy)
cli_client = None
try:
cli_client = importutils.import_object(
self.configuration.brcd_sb_connector,
ipaddress=fabric_ip,
username=fabric_user,
password=fabric_pwd,
port=fabric_port)
if not cli_client.is_supported_firmware():
msg = _("Unsupported firmware on switch %s. Make sure "
"switch is running firmware v6.4 or higher"
) % fabric_ip
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
except exception.BrocadeZoningCliException as brocade_ex:
raise exception.FCZoneDriverException(str(brocade_ex))
except Exception as e:
LOG.error(str(e))
msg = _("Failed to add zoning configuration %s"
) % str(e)
raise exception.FCZoneDriverException(msg)
cfgmap_from_fabric = self.get_active_zone_set(
fabric_ip, fabric_user, fabric_pwd, fabric_port)
zone_names = []
if cfgmap_from_fabric.get('zones'):
zone_names = cfgmap_from_fabric['zones'].keys()
# based on zoning policy, create zone member list and
# push changes to fabric.
for initiator_key in initiator_target_map.keys():
zone_map = {}
initiator = initiator_key.lower()
t_list = initiator_target_map[initiator_key]
if zoning_policy == 'initiator-target':
for t in t_list:
target = t.lower()
zone_members = [self.get_formatted_wwn(initiator),
self.get_formatted_wwn(target)]
zone_name = (self.configuration.zone_name_prefix
+ initiator.replace(':', '')
+ target.replace(':', ''))
if (
len(cfgmap_from_fabric) == 0 or (
zone_name not in zone_names)):
zone_map[zone_name] = zone_members
else:
# This is I-T zoning, skip if zone already exists.
LOG.info(_("Zone exists in I-T mode. "
"Skipping zone creation %s"), zone_name)
elif zoning_policy == 'initiator':
zone_members = [self.get_formatted_wwn(initiator)]
for t in t_list:
target = t.lower()
zone_members.append(self.get_formatted_wwn(target))
zone_name = self.configuration.zone_name_prefix \
+ initiator.replace(':', '')
if len(zone_names) > 0 and (zone_name in zone_names):
zone_members = zone_members + filter(
lambda x: x not in zone_members,
cfgmap_from_fabric['zones'][zone_name])
zone_map[zone_name] = zone_members
else:
msg = _("Zoning Policy: %s, not "
"recognized") % zoning_policy
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
LOG.info(_("Zone map to add: %s"), zone_map)
if len(zone_map) > 0:
try:
cli_client.add_zones(
zone_map, self.configuration.zone_activate)
cli_client.cleanup()
except exception.BrocadeZoningCliException as brocade_ex:
raise exception.FCZoneDriverException(str(brocade_ex))
except Exception as e:
LOG.error(str(e))
msg = _("Failed to add zoning configuration %s"
) % str(e)
raise exception.FCZoneDriverException(msg)
LOG.debug(_("Zones added successfully: %s"), zone_map)
@lockutils.synchronized('brcd', 'fcfabric-', True)
def delete_connection(self, fabric, initiator_target_map):
"""Concrete implementation of delete_connection.
Based on zoning policy and state of each I-T pair, list of zones
are created for deletion. The zones are either updated deleted based
on the policy and attach/detach state of each I-T pair.
:param fabric: Fabric name from cinder.conf file
:param initiator_target_map: Mapping of initiator to list of targets
"""
LOG.debug(_("Delete connection for fabric:%s"), fabric)
LOG.info(_("BrcdFCZoneDriver - Delete connection for I-T map: %s"),
initiator_target_map)
fabric_ip = self.configuration.safe_get(
'fc_fabric_address_' + fabric)
fabric_user = self.configuration.safe_get(
'fc_fabric_user_' + fabric)
fabric_pwd = self.configuration.safe_get(
'fc_fabric_password_' + fabric)
fabric_port = self.configuration.safe_get(
'fc_fabric_port_' + fabric)
zoning_policy = self.configuration.zoning_policy
zoning_policy_fab = self.configuration.safe_get(
'zoning_policy_' + fabric)
if zoning_policy_fab:
zoning_policy = zoning_policy_fab
LOG.info(_("Zoning policy for fabric %s"), zoning_policy)
conn = None
try:
conn = importutils.import_object(
self.configuration.brcd_sb_connector,
ipaddress=fabric_ip,
username=fabric_user,
password=fabric_pwd,
port=fabric_port)
if not conn.is_supported_firmware():
msg = _("Unsupported firmware on switch %s. Make sure "
"switch is running firmware v6.4 or higher"
) % fabric_ip
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
except exception.BrocadeZoningCliException as brocade_ex:
raise exception.FCZoneDriverException(str(brocade_ex))
except Exception as e:
LOG.error(str(e))
msg = _("Failed to delete zoning configuration %s"
) % str(e)
raise exception.FCZoneDriverException(msg)
cfgmap_from_fabric = self.get_active_zone_set(
fabric_ip, fabric_user, fabric_pwd, fabric_port)
zone_names = []
if cfgmap_from_fabric.get('zones'):
zone_names = cfgmap_from_fabric['zones'].keys()
# Based on zoning policy, get zone member list and push changes to
# fabric. This operation could result in an update for zone config
# with new member list or deleting zones from active cfg.
LOG.debug(_("zone config from Fabric: %s"), cfgmap_from_fabric)
for initiator_key in initiator_target_map.keys():
initiator = initiator_key.lower()
formatted_initiator = self.get_formatted_wwn(initiator)
zone_map = {}
zones_to_delete = []
t_list = initiator_target_map[initiator_key]
if zoning_policy == 'initiator-target':
# In this case, zone needs to be deleted.
for t in t_list:
target = t.lower()
zone_name = (
self.configuration.zone_name_prefix
+ initiator.replace(':', '')
+ target.replace(':', ''))
LOG.debug(_("Zone name to del: %s"), zone_name)
if len(zone_names) > 0 and (zone_name in zone_names):
# delete zone.
LOG.debug(("Added zone to delete to "
"list: %s"), zone_name)
zones_to_delete.append(zone_name)
elif zoning_policy == 'initiator':
zone_members = [formatted_initiator]
for t in t_list:
target = t.lower()
zone_members.append(self.get_formatted_wwn(target))
zone_name = self.configuration.zone_name_prefix \
+ initiator.replace(':', '')
if (zone_names and (zone_name in zone_names)):
filtered_members = filter(
lambda x: x not in zone_members,
cfgmap_from_fabric['zones'][zone_name])
# The assumption here is that initiator is always there
# in the zone as it is 'initiator' policy. We find the
# filtered list and if it is non-empty, add initiator
# to it and update zone if filtered list is empty, we
# remove that zone.
LOG.debug(_("Zone delete - I mode: "
"filtered targets:%s"), filtered_members)
if filtered_members:
filtered_members.append(formatted_initiator)
LOG.debug(_("Filtered zone members to "
"update: %s"), filtered_members)
zone_map[zone_name] = filtered_members
LOG.debug(_("Filtered zone Map to "
"update: %s"), zone_map)
else:
zones_to_delete.append(zone_name)
else:
LOG.info(_("Zoning Policy: %s, not "
"recognized"), zoning_policy)
LOG.debug(_("Final Zone map to update: %s"), zone_map)
LOG.debug(_("Final Zone list to delete: %s"), zones_to_delete)
try:
# Update zone membership.
if zone_map:
conn.add_zones(
zone_map, self.configuration.zone_activate)
# Delete zones ~sk.
if zones_to_delete:
zone_name_string = ''
num_zones = len(zones_to_delete)
for i in range(0, num_zones):
if i == 0:
zone_name_string = (
'%s%s' % (
zone_name_string, zones_to_delete[i]))
else:
zone_name_string = '%s%s%s' % (
zone_name_string, ';', zones_to_delete[i])
conn.delete_zones(
zone_name_string, self.configuration.zone_activate)
conn.cleanup()
except Exception as e:
LOG.error(str(e))
msg = _("Failed to update or delete zoning configuration")
raise exception.FCZoneDriverException(msg)
def get_san_context(self, target_wwn_list):
"""Lookup SAN context for visible end devices.
Look up each SAN configured and return a map of SAN (fabric IP) to
list of target WWNs visible to the fabric.
"""
# TODO(Santhosh Kolathur): consider refactoring to use lookup service.
formatted_target_list = []
fabric_map = {}
fabrics = self.configuration.fc_fabric_names.split(',')
LOG.debug(_("Fabric List: %s"), fabrics)
LOG.debug(_("Target wwn List: %s"), target_wwn_list)
if len(fabrics) > 0:
for t in target_wwn_list:
formatted_target_list.append(self.get_formatted_wwn(t.lower()))
LOG.debug(_("Formatted Target wwn List:"
" %s"), formatted_target_list)
for fabric_name in fabrics:
fabric_ip = self.configuration.safe_get(
'fc_fabric_address_' + fabric_name)
fabric_user = self.configuration.safe_get(
'fc_fabric_user_' + fabric_name)
fabric_pwd = self.configuration.safe_get(
'fc_fabric_password_' + fabric_name)
fabric_port = self.configuration.safe_get(
'fc_fabric_port_' + fabric_name)
conn = None
try:
conn = importutils.import_object(
self.configuration.brcd_sb_connector,
ipaddress=fabric_ip,
username=fabric_user,
password=fabric_pwd,
port=fabric_port)
if not conn.is_supported_firmware():
msg = _("Unsupported firmware on switch %s. Make sure "
"switch is running firmware v6.4 or higher"
) % fabric_ip
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
except exception.BrocadeZoningCliException as brocade_ex:
raise exception.FCZoneDriverException(str(brocade_ex))
except Exception as e:
LOG.error(str(e))
msg = _("Failed to get SAN context %s"
) % str(e)
raise exception.FCZoneDriverException(msg)
# Get name server data from fabric and get the targets
# logged in.
nsinfo = None
try:
nsinfo = conn.get_nameserver_info()
LOG.debug(_("name server info from fabric:%s"), nsinfo)
conn.cleanup()
except exception.BrocadeZoningCliException as ex:
with excutils.save_and_reraise_exception():
LOG.error(_("Error getting name server "
"info: %s"), str(ex))
except Exception as e:
msg = _("Failed to get name server info:%s") % str(e)
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
visible_targets = filter(
lambda x: x in formatted_target_list,
nsinfo)
if visible_targets:
LOG.info(_("Filtered targets for SAN is: %s"),
{fabric_name: visible_targets})
# getting rid of the ':' before returning
for idx, elem in enumerate(visible_targets):
visible_targets[idx] = str(
visible_targets[idx]).replace(':', '')
fabric_map[fabric_name] = visible_targets
else:
LOG.debug(_("No targets are in the nameserver for SAN %s"),
fabric_name)
LOG.debug(_("Return SAN context output:%s"), fabric_map)
return fabric_map
def get_active_zone_set(self, fabric_ip,
fabric_user, fabric_pwd, fabric_port):
"""Gets active zone config from fabric."""
cfgmap = {}
conn = None
try:
LOG.debug(_("Southbound connector:"
" %s"), self.configuration.brcd_sb_connector)
conn = importutils.import_object(
self.configuration.brcd_sb_connector,
ipaddress=fabric_ip, username=fabric_user,
password=fabric_pwd, port=fabric_port)
if not conn.is_supported_firmware():
msg = _("Unsupported firmware on switch %s. Make sure "
"switch is running firmware v6.4 or higher"
) % fabric_ip
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
cfgmap = conn.get_active_zone_set()
conn.cleanup()
except exception.BrocadeZoningCliException as brocade_ex:
raise exception.FCZoneDriverException(str(brocade_ex))
except Exception as e:
msg = _("Failed to access active zoning configuration:%s") % str(e)
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
LOG.debug(_("Active zone set from fabric: %s"), cfgmap)
return cfgmap

View File

@ -0,0 +1,47 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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.
#
"""
Common constants used by Brocade FC Zone Driver.
"""
YES = 'y'
ACTIVE_ZONE_CONFIG = 'active_zone_config'
CFG_ZONESET = 'cfg:'
CFG_ZONES = 'zones'
OPENSTACK_CFG_NAME = 'OpenStack_Cfg'
SUCCESS = 'Success'
TRANS_ABORTABLE = 'It is abortable'
"""
CLI Commands for FC zoning operations.
"""
GET_ACTIVE_ZONE_CFG = 'cfgactvshow'
ZONE_CREATE = 'zonecreate '
ZONESET_CREATE = 'cfgcreate '
CFG_SAVE = 'cfgsave'
CFG_ADD = 'cfgadd '
ACTIVATE_ZONESET = 'cfgenable '
DEACTIVATE_ZONESET = 'cfgdisable'
CFG_DELETE = 'cfgdelete '
CFG_REMOVE = 'cfgremove '
ZONE_DELETE = 'zonedelete '
CFG_SHOW_TRANS = 'cfgtransshow'
CFG_ZONE_TRANS_ABORT = 'cfgtransabort'
NS_SHOW = 'nsshow'
NS_CAM_SHOW = 'nscamshow'

View File

@ -0,0 +1,38 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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
san_context_opts = [
cfg.StrOpt('fc_fabric_names',
default=None,
help='Comma separated list of fibre channel fabric names.'
' This list of names is used to retrieve other SAN credentials'
' for connecting to each SAN fabric'),
]
CONF = cfg.CONF
CONF.register_opts(san_context_opts)
class FCCommon(object):
"""Common interface for FC operations."""
def __init__(self, **kwargs):
pass

View File

@ -0,0 +1,106 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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.
#
"""
Base Zone Driver is responsible to manage access control using FC zoning
Vendor specific implementations should extend this class to provide
concrete implementation for add_connection and delete_connection
interfaces.
**Related Flags**
:zoning_policy: Used by: class: 'FCZoneDriver'. Defaults to 'none'
:zone_driver: Used by: class: 'FCZoneDriver'. Defaults to 'none'
"""
from oslo.config import cfg
from cinder.openstack.common import log as logging
from cinder.zonemanager.drivers.fc_common import FCCommon
LOG = logging.getLogger(__name__)
fc_zone_opts = [
cfg.BoolOpt('zone_activate',
default=True,
help="Indicates whether zone should be activated or not"),
cfg.StrOpt('zone_name_prefix',
default="openstack",
help="A prefix to be used when naming zone"),
]
CONF = cfg.CONF
CONF.register_opts(fc_zone_opts)
class FCZoneDriver(FCCommon):
"""Interface to manage Connection control during attach/detach."""
def __init__(self, **kwargs):
super(FCZoneDriver, self).__init__(**kwargs)
LOG.debug(_("Initializing FCZoneDriver"))
def add_connection(self, fabric, initiator_target_map):
"""Add connection control.
Abstract method to add connection control.
All implementing drivers should provide concrete implementation
for this API.
:param fabric: Fabric name from cinder.conf file
:param initiator_target_map: Mapping of initiator to list of targets
Example initiator_target_map:
{
'10008c7cff523b01': ['20240002ac000a50', '20240002ac000a40']
}
Note that WWPN can be in lower or upper case and can be
':' separated strings
"""
raise NotImplementedError()
def delete_connection(self, fabric, initiator_target_map):
"""Delete connection control.
Abstract method to remove connection control.
All implementing drivers should provide concrete implementation
for this API.
:param fabric: Fabric name from cinder.conf file
:param initiator_target_map: Mapping of initiator to list of targets
Example initiator_target_map:
{
'10008c7cff523b01': ['20240002ac000a50', '20240002ac000a40']
}
Note that WWPN can be in lower or upper case and can be
':' separated strings
"""
raise NotImplementedError()
def get_san_context(self, target_wwn_list):
"""Get SAN context for end devices.
Abstract method to get SAN contexts for given list of end devices
All implementing drivers should provide concrete implementation
for this API.
:param fabric: Fabric name from cinder.conf file
:param initiator_target_map: Mapping of initiator to list of targets
Example initiator_target_map: ['20240002ac000a50', '20240002ac000a40']
Note that WWPN can be in lower or upper case and can be
':' separated strings
"""
raise NotImplementedError()

View File

@ -0,0 +1,97 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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.
#
"""
Base Lookup Service for name server lookup to find the initiator to target port
mapping for available SAN contexts.
Vendor specific lookup classes are expected to implement the interfaces
defined in this class.
"""
from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import importutils
from cinder.openstack.common import log as logging
LOG = logging.getLogger(__name__)
lookup_service_opts = [
cfg.StrOpt('fc_san_lookup_service',
default='cinder.zonemanager.drivers.brocade'
'.brcd_fc_san_lookup_service.BrcdFCSanLookupService',
help='FC San Lookup Service'),
]
CONF = cfg.CONF
CONF.register_opts(lookup_service_opts)
class FCSanLookupService(object):
"""Base Lookup Service.
Base Lookup Service for name server lookup to find the initiator to
target port mapping for available SAN contexts.
"""
lookup_service = None
def __init__(self, **kwargs):
self.configuration = kwargs.get('configuration', None)
if self.configuration:
self.configuration.append_config_values(lookup_service_opts)
def get_device_mapping_from_network(self, initiator_list, target_list):
"""Get device mapping from FC network.
Gets a filtered list of initiator ports and target ports for each SAN
available.
:param initiator_list list of initiator port WWN
:param target_list list of target port WWN
:return device wwn map in following format
{
<San name>: {
'initiator_port_wwn_list':
('200000051E55A100', '200000051E55A121'..)
'target_port_wwn_list':
('100000051E55A100', '100000051E55A121'..)
}
}
:raise Exception when a lookup service implementation is not specified
in cinder.conf:fc_san_lookup_service
"""
# Initialize vendor specific implementation of FCZoneDriver
if (self.configuration.fc_san_lookup_service):
lookup_service = self.configuration.fc_san_lookup_service
LOG.debug(_("Lookup service to invoke: "
"%s"), lookup_service)
self.lookup_service = importutils.import_object(
lookup_service, configuration=self.configuration)
else:
msg = _("Lookup service not configured. Config option for "
"fc_san_lookup_service need to specify a concrete "
"implementation of lookup service")
LOG.error(msg)
raise exception.FCSanLookupServiceException(msg)
try:
device_map = self.lookup_service.get_device_mapping_from_network(
initiator_list, target_list)
except Exception as e:
LOG.error(str(e))
raise exception.FCSanLookupServiceException(str(e))
return device_map

View File

@ -0,0 +1,205 @@
# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# 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.
#
"""
ZoneManager is responsible to manage access control using FC zoning
when zoning mode is set as 'fabric'.
ZoneManager provides interfaces to add connection and remove connection
for given initiator and target list associated with a FC volume attach and
detach operation.
**Related Flags**
:zone_driver: Used by:class:`ZoneManager`.
Defaults to
`cinder.zonemanager.drivers.brocade.brcd_fc_zone_driver.BrcdFCZoneDriver`
:zoning_policy: Used by: class: 'ZoneManager'. Defaults to 'none'
"""
from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import importutils
from cinder.openstack.common import log as logging
LOG = logging.getLogger(__name__)
zone_manager_opts = [
cfg.StrOpt('zone_driver',
default='cinder.zonemanager.drivers.brocade.brcd_fc_zone_driver'
'.BrcdFCZoneDriver',
help='FC Zone Driver responsible for zone management'),
cfg.StrOpt('zoning_policy',
default='initiator-target',
help='Zoning policy configured by user'),
]
CONF = cfg.CONF
CONF.register_opts(zone_manager_opts)
class ZoneManager:
"""Manages Connection control during attach/detach."""
driver = None
fabric_names = []
def __init__(self, **kwargs):
"""Load the driver from the one specified in args, or from flags."""
self.configuration = kwargs.get('configuration', None)
if self.configuration:
self.configuration.append_config_values(zone_manager_opts)
zone_driver = self.configuration.zone_driver
LOG.debug(_("Zone Driver from config: {%s}"), zone_driver)
# Initialize vendor specific implementation of FCZoneDriver
self.driver = importutils.import_object(
zone_driver,
configuration=self.configuration)
def get_zoning_state_ref_count(self, initiator_wwn, target_wwn):
"""Zone management state check.
Performs state check for given I-T pair to return the current count of
active attach for the pair.
"""
# TODO(sk): ref count state management
count = 0
# check the state for I-T pair
return count
def add_connection(self, initiator_target_map):
"""Add connection control.
Adds connection control for the given initiator target map.
initiator_target_map - each initiator WWN mapped to a list of one
or more target WWN:
eg:
{
'10008c7cff523b01': ['20240002ac000a50', '20240002ac000a40']
}
"""
connected_fabric = None
try:
for initiator in initiator_target_map.keys():
target_list = initiator_target_map[initiator]
LOG.debug(_("Target List :%s"), {initiator: target_list})
# get SAN context for the target list
fabric_map = self.driver.get_san_context(target_list)
LOG.debug(_("Fabric Map after context lookup:%s"), fabric_map)
# iterate over each SAN and apply connection control
for fabric in fabric_map.keys():
connected_fabric = fabric
t_list = fabric_map[fabric]
# get valid I-T map to add connection control
i_t_map = {initiator: t_list}
valid_i_t_map = self.get_valid_initiator_target_map(
i_t_map, True)
LOG.info(_("Final filtered map for fabric: %s"),
{fabric: valid_i_t_map})
# Call driver to add connection control
self.driver.add_connection(fabric, valid_i_t_map)
LOG.info(_("Add Connection: Finished iterating "
"over all target list"))
except Exception as e:
msg = _("Failed adding connection for fabric=%(fabric)s: "
"Error:%(err)s") % {'fabric': connected_fabric,
'err': str(e)}
LOG.error(msg)
raise exception.ZoneManagerException(reason=msg)
def delete_connection(self, initiator_target_map):
"""Delete connection.
Updates/deletes connection control for the given initiator target map.
initiator_target_map - each initiator WWN mapped to a list of one
or more target WWN:
eg:
{
'10008c7cff523b01': ['20240002ac000a50', '20240002ac000a40']
}
"""
connected_fabric = None
try:
for initiator in initiator_target_map.keys():
target_list = initiator_target_map[initiator]
LOG.info(_("Delete connection Target List:%s"),
{initiator: target_list})
# get SAN context for the target list
fabric_map = self.driver.get_san_context(target_list)
LOG.debug(_("Delete connection Fabric Map from SAN "
"context: %s"), fabric_map)
# iterate over each SAN and apply connection control
for fabric in fabric_map.keys():
connected_fabric = fabric
t_list = fabric_map[fabric]
# get valid I-T map to add connection control
i_t_map = {initiator: t_list}
valid_i_t_map = self.get_valid_initiator_target_map(
i_t_map, False)
LOG.info(_("Final filtered map for delete "
"connection: %s"), valid_i_t_map)
# Call driver to delete connection control
if len(valid_i_t_map) > 0:
self.driver.delete_connection(fabric, valid_i_t_map)
LOG.debug(_("Delete Connection - Finished iterating over all"
" target list"))
except Exception as e:
msg = _("Failed removing connection for fabric=%(fabric)s: "
"Error:%(err)s") % {'fabric': connected_fabric,
'err': str(e)}
LOG.error(msg)
raise exception.ZoneManagerException(reason=msg)
def get_valid_initiator_target_map(self, initiator_target_map,
add_control):
"""Reference count check for end devices.
Looks up the reference count for each initiator-target pair from the
map and returns a filtered list based on the operation type
add_control - operation type can be true for add connection control
and false for remove connection control
"""
filtered_i_t_map = {}
for initiator in initiator_target_map.keys():
t_list = initiator_target_map[initiator]
for target in t_list:
count = self.get_zoning_state_ref_count(initiator, target)
if add_control:
if count > 0:
t_list.remove(target)
# update count = count + 1
else:
if count > 1:
t_list.remove(target)
# update count = count - 1
if t_list:
filtered_i_t_map[initiator] = t_list
else:
LOG.info(_("No targets to add or remove connection for "
"I: %s"), initiator)
return filtered_i_t_map

View File

@ -1762,6 +1762,59 @@
# (boolean value) # (boolean value)
#volume_service_inithost_offload=false #volume_service_inithost_offload=false
# FC Zoning mode configured (string value)
#zoning_mode=none
#
# Options defined in cinder.zonemanager.drivers.brocade.brcd_fc_zone_driver
#
# Southbound connector for zoning operation (string value)
#brcd_sb_connector=cinder.zonemanager.drivers.brocade.brcd_fc_zone_client_cli.BrcdFCZoneClientCLI
#
# Options defined in cinder.zonemanager.drivers.fc_common
#
# Comma separated list of fibre channel fabric names. This
# list of names is used to retrieve other SAN credentials for
# connecting to each SAN fabric (string value)
#fc_fabric_names=<None>
#
# Options defined in cinder.zonemanager.drivers.fc_zone_driver
#
# Indicates whether zone should be activated or not (boolean
# value)
#zone_activate=true
# A prefix to be used when naming zone (string value)
#zone_name_prefix=openstack
#
# Options defined in cinder.zonemanager.fc_san_lookup_service
#
# FC San Lookup Service (string value)
#fc_san_lookup_service=cinder.zonemanager.drivers.brocade.brcd_fc_san_lookup_service.BrcdFCSanLookupService
#
# Options defined in cinder.zonemanager.fc_zone_manager
#
# FC Zone Driver responsible for zone management (string
# value)
#zone_driver=cinder.zonemanager.drivers.brocade.brcd_fc_zone_driver.BrcdFCZoneDriver
# Zoning policy configured by user (string value)
#zoning_policy=initiator-target
[ssl] [ssl]