From: John McDonough Date: Wed, 2 Apr 2014 22:47:06 +0000 (-0400) Subject: Add Cisco FC Zoning plugin to the FC ZoneManager X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=230f115d349f1901dc12876680685ff88b33530c;p=openstack-build%2Fcinder-build.git Add Cisco FC Zoning plugin to the FC ZoneManager The Cisco FC Zoning plugin allows for the automated creation, deletion and modification of zones in zonesets. The Cisco FC zoning plugin supports both basic and enhanced zoning and supports vsans. Implements: blueprint cisco-fc-zone-driver Change-Id: Icf176b6f46f1b1662f0a99ffaa226789661f611e --- diff --git a/cinder/exception.py b/cinder/exception.py index bba8210c1..6ea0d0d79 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -773,6 +773,10 @@ class BrocadeZoningCliException(CinderException): message = _("Fibre Channel Zoning CLI error: %(reason)s") +class CiscoZoningCliException(CinderException): + message = _("Fibre Channel Zoning CLI error: %(reason)s") + + class NetAppDriverException(VolumeDriverException): message = _("NetApp Cinder Driver exception.") diff --git a/cinder/tests/zonemanager/test_cisco_fc_san_lookup_service.py b/cinder/tests/zonemanager/test_cisco_fc_san_lookup_service.py new file mode 100644 index 000000000..acf8146fd --- /dev/null +++ b/cinder/tests/zonemanager/test_cisco_fc_san_lookup_service.py @@ -0,0 +1,133 @@ +# (c) Copyright 2014 Cisco Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +"""Unit tests for Cisco fc san lookup service.""" + +import mock +from oslo.config import cfg + +from cinder import exception +from cinder import test +from cinder.volume import configuration as conf +import cinder.zonemanager.drivers.cisco.cisco_fc_san_lookup_service \ + as cisco_lookup +from cinder.zonemanager.utils import get_formatted_wwn + +nsshow = '20:1a:00:05:1e:e8:e3:29' +switch_data = ['VSAN 304\n', + '------------------------------------------------------\n', + 'FCID TYPE PWWN (VENDOR) \n', + '------------------------------------------------------\n', + '0x030001 N 20:1a:00:05:1e:e8:e3:29 (Cisco) ipfc\n', + '0x030101 NL 10:00:00:00:77:99:60:2c (Interphase)\n', + '0x030200 N 10:00:00:49:c9:28:c7:01\n'] + +nsshow_data = ['10:00:8c:7c:ff:52:3b:01', '20:24:00:02:ac:00:0a:50'] + +_device_map_to_verify = { + '304': { + 'initiator_port_wwn_list': ['10008c7cff523b01'], + 'target_port_wwn_list': ['20240002ac000a50']}} + + +class TestCiscoFCSanLookupService(cisco_lookup.CiscoFCSanLookupService, + test.TestCase): + + def setUp(self): + super(TestCiscoFCSanLookupService, self).setUp() + self.configuration = conf.Configuration(None) + self.configuration.set_default('fc_fabric_names', 'CISCO_FAB_2', + 'fc-zone-manager') + self.configuration.fc_fabric_names = 'CISCO_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('cisco_fc_fabric_address', + default='172.24.173.142', help='')) + fc_fabric_opts.append(cfg.StrOpt('cisco_fc_fabric_user', + default='admin', help='')) + fc_fabric_opts.append(cfg.StrOpt('cisco_fc_fabric_password', + default='admin1234', help='', + secret=True)) + fc_fabric_opts.append(cfg.IntOpt('cisco_fc_fabric_port', + default=22, help='')) + fc_fabric_opts.append(cfg.StrOpt('cisco_zoning_vsan', + default='304', help='')) + config = conf.Configuration(fc_fabric_opts, 'CISCO_FAB_2') + self.fabric_configs = {'CISCO_FAB_2': config} + + @mock.patch.object(cisco_lookup.CiscoFCSanLookupService, + 'get_nameserver_info') + def test_get_device_mapping_from_network(self, get_nameserver_info_mock): + initiator_list = ['10008c7cff523b01'] + target_list = ['20240002ac000a50', '20240002ac000a40'] + 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) + + @mock.patch.object(cisco_lookup.CiscoFCSanLookupService, + '_get_switch_info') + 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', + '10:00:00:49:c9:28:c7:01'] + get_switch_data_mock.return_value = (switch_data) + ns_info_list = self.get_nameserver_info('304') + self.assertEqual(ns_info_list, ns_info_list_expected) + + 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', + '10:00:00:49:c9:28:c7:01'] + 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(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 = '' diff --git a/cinder/tests/zonemanager/test_cisco_fc_zone_client_cli.py b/cinder/tests/zonemanager/test_cisco_fc_zone_client_cli.py new file mode 100644 index 000000000..f1e32486a --- /dev/null +++ b/cinder/tests/zonemanager/test_cisco_fc_zone_client_cli.py @@ -0,0 +1,233 @@ +# (c) Copyright 2014 Cisco Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +"""Unit tests for Cisco fc zone client cli.""" + +from mock import patch + +from cinder import exception +from cinder.openstack.common import processutils +from cinder import test +from cinder.zonemanager.drivers.cisco.cisco_fc_zone_client_cli \ + import CiscoFCZoneClientCLI +import cinder.zonemanager.drivers.cisco.fc_zone_constants as ZoneConstant + +nsshow = '20:1a:00:05:1e:e8:e3:29' +switch_data = ['VSAN 303\n', + '----------------------------------------------------------\n', + 'FCID TYPE PWWN (VENDOR) FC4-TYPE:FEATURE\n', + '----------------------------------------------------------\n', + '0x030001 N 20:1a:00:05:1e:e8:e3:29 (Cisco) ipfc\n', + '0x030101 NL 10:00:00:00:77:99:60:2c (Interphase)\n', + '0x030200 NL 10:00:00:49:c9:28:c7:01\n'] + +cfgactv = ['zoneset name OpenStack_Cfg vsan 303\n', + 'zone name openstack50060b0000c26604201900051ee8e329 vsan 303\n', + 'pwwn 50:06:0b:00:00:c2:66:04\n', + 'pwwn 20: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'} + +zoning_status_data_basic = [ + 'VSAN: 303 default-zone: deny distribute: active only Interop: default\n', + ' mode: basic merge-control: allow\n', + ' session: none\n', + ' hard-zoning: enabled broadcast: unsupported\n', + ' smart-zoning: disabled\n', + ' rscn-format: fabric-address\n', + 'Default zone:\n', + ' qos: none broadcast: unsupported ronly: unsupported\n', + 'Full Zoning Database :\n', + ' DB size: 220 bytes\n', + ' Zonesets:2 Zones:2 Aliases: 0\n', + 'Active Zoning Database :\n', + ' DB size: 80 bytes\n', + ' Name: test-zs-test Zonesets:1 Zones:1\n', + 'Status:\n'] + +zoning_status_basic = {'mode': 'basic', 'session': 'none'} + +zoning_status_data_enhanced_nosess = [ + 'VSAN: 303 default-zone: deny distribute: active only Interop: default\n', + ' mode: enhanced merge-control: allow\n', + ' session: none\n', + ' hard-zoning: enabled broadcast: unsupported\n', + ' smart-zoning: disabled\n', + ' rscn-format: fabric-address\n', + 'Default zone:\n', + ' qos: none broadcast: unsupported ronly: unsupported\n', + 'Full Zoning Database :\n', + ' DB size: 220 bytes\n', + ' Zonesets:2 Zones:2 Aliases: 0\n', + 'Active Zoning Database :\n', + ' DB size: 80 bytes\n', + ' Name: test-zs-test Zonesets:1 Zones:1\n', + 'Status:\n'] + +zoning_status_enhanced_nosess = {'mode': 'enhanced', 'session': 'none'} + +zoning_status_data_enhanced_sess = [ + 'VSAN: 303 default-zone: deny distribute: active only Interop: default\n', + ' mode: enhanced merge-control: allow\n', + ' session: otherthannone\n', + ' hard-zoning: enabled broadcast: unsupported\n', + ' smart-zoning: disabled\n', + ' rscn-format: fabric-address\n', + 'Default zone:\n', + ' qos: none broadcast: unsupported ronly: unsupported\n', + 'Full Zoning Database :\n', + ' DB size: 220 bytes\n', + ' Zonesets:2 Zones:2 Aliases: 0\n', + 'Active Zoning Database :\n', + ' DB size: 80 bytes\n', + ' Name: test-zs-test Zonesets:1 Zones:1\n', + 'Status:\n'] + +zoning_status_enhanced_sess = {'mode': 'enhanced', 'session': 'otherthannone'} + +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' + + +class TestCiscoFCZoneClientCLI(CiscoFCZoneClientCLI, test.TestCase): + + def setUp(self): + super(TestCiscoFCZoneClientCLI, self).setUp() + self.fabric_vsan = '303' + + # override some of the functions + def __init__(self, *args, **kwargs): + test.TestCase.__init__(self, *args, **kwargs) + + @patch.object(CiscoFCZoneClientCLI, '_get_switch_info') + def test_get_active_zone_set(self, get_switch_info_mock): + cmd_list = [ZoneConstant.GET_ACTIVE_ZONE_CFG, self.fabric_vsan, + ' | no-more'] + get_switch_info_mock.return_value = cfgactv + 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(CiscoFCZoneClientCLI, '_run_ssh') + def test_get_active_zone_set_ssh_error(self, run_ssh_mock): + run_ssh_mock.side_effect = processutils.ProcessExecutionError + self.assertRaises(exception.CiscoZoningCliException, + self.get_active_zone_set) + + @patch.object(CiscoFCZoneClientCLI, '_get_switch_info') + def test_get_zoning_status_basic(self, get_zoning_status_mock): + cmd_list = [ZoneConstant.GET_ZONE_STATUS, self.fabric_vsan] + get_zoning_status_mock.return_value = zoning_status_data_basic + zoning_status_returned = self.get_zoning_status() + get_zoning_status_mock.assert_called_once_with(cmd_list) + self.assertDictMatch(zoning_status_returned, zoning_status_basic) + + @patch.object(CiscoFCZoneClientCLI, '_get_switch_info') + def test_get_zoning_status_enhanced_nosess(self, get_zoning_status_mock): + cmd_list = [ZoneConstant.GET_ZONE_STATUS, self.fabric_vsan] + get_zoning_status_mock.return_value =\ + zoning_status_data_enhanced_nosess + zoning_status_returned = self.get_zoning_status() + get_zoning_status_mock.assert_called_once_with(cmd_list) + self.assertDictMatch(zoning_status_returned, + zoning_status_enhanced_nosess) + + @patch.object(CiscoFCZoneClientCLI, '_get_switch_info') + def test_get_zoning_status_enhanced_sess(self, get_zoning_status_mock): + cmd_list = [ZoneConstant.GET_ZONE_STATUS, self.fabric_vsan] + get_zoning_status_mock.return_value = zoning_status_data_enhanced_sess + zoning_status_returned = self.get_zoning_status() + get_zoning_status_mock.assert_called_once_with(cmd_list) + self.assertDictMatch(zoning_status_returned, + zoning_status_enhanced_sess) + + @patch.object(CiscoFCZoneClientCLI, '_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'] + 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(CiscoFCZoneClientCLI, '_run_ssh') + def test_get_nameserver_info_ssh_error(self, run_ssh_mock): + run_ssh_mock.side_effect = processutils.ProcessExecutionError + self.assertRaises(exception.CiscoZoningCliException, + self.get_nameserver_info) + + @patch.object(CiscoFCZoneClientCLI, '_run_ssh') + def test__cfg_save(self, run_ssh_mock): + cmd_list = ['copy', 'running-config', 'startup-config'] + self._cfg_save() + run_ssh_mock.assert_called_once_with(cmd_list, True, 1) + + @patch.object(CiscoFCZoneClientCLI, '_run_ssh') + def test__get_switch_info(self, run_ssh_mock): + cmd_list = [ZoneConstant.FCNS_SHOW, self.fabric_vsan] + 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): + 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) + + +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 = '' diff --git a/cinder/tests/zonemanager/test_cisco_fc_zone_driver.py b/cinder/tests/zonemanager/test_cisco_fc_zone_driver.py new file mode 100644 index 000000000..7083e86bc --- /dev/null +++ b/cinder/tests/zonemanager/test_cisco_fc_zone_driver.py @@ -0,0 +1,208 @@ +# (c) Copyright 2014 Cisco Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +"""Unit tests for Cisco FC zone driver.""" + +from oslo.config import cfg + +from cinder import exception +from cinder.openstack.common import importutils +from cinder.openstack.common import processutils +from cinder import test +from cinder.volume import configuration as conf + +_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'])}, + 'active_zone_config': 'cfg1'} +_activate = True +_zone_name = 'openstack10008c7cff523b0120240002ac000a50' +_target_ns_map = {'100000051e55a100': ['20240002ac000a50']} +_zoning_status = {'mode': 'basis', 'session': 'none'} +_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 = { + '304': { + 'initiator_port_wwn_list': [ + '10008c7cff523b01'], 'target_port_wwn_list': ['20240002ac000a50']}} +_fabric_wwn = '304' + + +class CiscoFcZoneDriverBaseTest(object): + + def setup_config(self, is_normal, mode): + fc_test_opts = [ + cfg.StrOpt('fc_fabric_address_CISCO_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.zonemanager.' + 'test_cisco_fc_zone_driver.' + 'FakeCiscoFCZoneDriver') + configuration.cisco_sb_connector = ('cinder.tests.zonemanager.' + 'test_cisco_fc_zone_driver' + '.FakeCiscoFCZoneClientCLI') + configuration.zoning_policy = 'initiator-target' + configuration.zone_activate = True + configuration.zone_name_prefix = 'openstack' + configuration.fc_san_lookup_service = ('cinder.tests.zonemanager.' + 'test_cisco_fc_zone_driver.' + 'FakeCiscoFCSanLookupService') + + configuration.fc_fabric_names = 'CISCO_FAB_1' + configuration.fc_fabric_address_CISCO_FAB_1 = '172.21.60.220' + if (is_normal): + configuration.fc_fabric_user_CISCO_FAB_1 = 'admin' + else: + configuration.fc_fabric_user_CISCO_FAB_1 = 'invaliduser' + configuration.fc_fabric_password_CISCO_FAB_1 = 'admin1234' + + if (mode == 1): + configuration.zoning_policy_CISCO_FAB_1 = 'initiator-target' + elif (mode == 2): + configuration.zoning_policy_CISCO_FAB_1 = 'initiator' + else: + configuration.zoning_policy_CISCO_FAB_1 = 'initiator-target' + configuration.zone_activate_CISCO_FAB_1 = True + configuration.zone_name_prefix_CISCO_FAB_1 = 'openstack' + configuration.zoning_vsan_CISCO_FAB_1 = '304' + return configuration + + +class TestCiscoFcZoneDriver(CiscoFcZoneDriverBaseTest, test.TestCase): + + def setUp(self): + super(TestCiscoFcZoneDriver, 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.cisco.cisco_fc_zone_driver' + '.CiscoFCZoneDriver', configuration=config) + + def fake_get_active_zone_set(self, fabric_ip, fabric_user, fabric_pwd, + zoning_vsan): + return GlobalVars._active_cfg + + def fake_get_san_context(self, target_wwn_list): + fabric_map = {} + return fabric_map + + def test_delete_connection(self): + GlobalVars._is_normal_test = True + GlobalVars._active_cfg = _active_cfg_before_delete + self.driver.delete_connection( + 'CISCO_FAB_1', _initiator_target_map) + self.assertFalse(_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( + 'CISCO_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, + 'CISCO_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, + 'CISCO_FAB_1', + _initiator_target_map) + + +class FakeCiscoFCZoneClientCLI(object): + def __init__(self, ipaddress, username, password, port, vsan): + if not GlobalVars._is_normal_test: + raise processutils.ProcessExecutionError( + "Unable to connect to fabric") + + def get_active_zone_set(self): + 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 get_nameserver_info(self): + return _target_ns_map + + def get_zoning_status(self): + return _zoning_status + + def close_connection(self): + pass + + def cleanup(self): + pass + + +class FakeCiscoFCSanLookupService(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 + global _zoning_status + _zoning_status = {} diff --git a/cinder/tests/zonemanager/test_cisco_lookup_service.py b/cinder/tests/zonemanager/test_cisco_lookup_service.py new file mode 100644 index 000000000..ea35beca7 --- /dev/null +++ b/cinder/tests/zonemanager/test_cisco_lookup_service.py @@ -0,0 +1,96 @@ +# (c) Copyright 2014 Cisco Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +"""Unit tests for Cisco FC san lookup service.""" + +from cinder import exception +from cinder import test +from cinder.volume import configuration as conf +from cinder.zonemanager.fc_san_lookup_service import FCSanLookupService + +_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.zonemanager' + '.test_cisco_lookup_service' + '.FakeCiscoFCSanLookupService') + 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 FakeCiscoFCSanLookupService(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 diff --git a/cinder/zonemanager/drivers/cisco/__init__.py b/cinder/zonemanager/drivers/cisco/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/zonemanager/drivers/cisco/cisco_fabric_opts.py b/cinder/zonemanager/drivers/cisco/cisco_fabric_opts.py new file mode 100644 index 000000000..5bd01d1b9 --- /dev/null +++ b/cinder/zonemanager/drivers/cisco/cisco_fabric_opts.py @@ -0,0 +1,58 @@ +# (c) Copyright 2014 Cisco Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from oslo.config import cfg + +from cinder.volume.configuration import Configuration + +cisco_zone_opts = [ + cfg.StrOpt('cisco_fc_fabric_address', + default='', + help='Management IP of fabric'), + cfg.StrOpt('cisco_fc_fabric_user', + default='', + help='Fabric user ID'), + cfg.StrOpt('cisco_fc_fabric_password', + default='', + help='Password for user', + secret=True), + cfg.IntOpt('cisco_fc_fabric_port', + default=22, + help='Connecting port'), + cfg.StrOpt('cisco_zoning_policy', + default='initiator-target', + help='overridden zoning policy'), + cfg.BoolOpt('cisco_zone_activate', + default=True, + help='overridden zoning activation state'), + cfg.StrOpt('cisco_zone_name_prefix', + default=None, + help='overridden zone name prefix'), + cfg.StrOpt('cisco_zoning_vsan', + default=None, + help='VSAN of the Fabric'), +] + +CONF = cfg.CONF +CONF.register_opts(cisco_zone_opts, 'CISCO_FABRIC_EXAMPLE') + + +def load_fabric_configurations(fabric_names): + fabric_configs = {} + for fabric_name in fabric_names: + config = Configuration(cisco_zone_opts, fabric_name) + fabric_configs[fabric_name] = config + + return fabric_configs diff --git a/cinder/zonemanager/drivers/cisco/cisco_fc_san_lookup_service.py b/cinder/zonemanager/drivers/cisco/cisco_fc_san_lookup_service.py new file mode 100644 index 000000000..eaf5b3dc9 --- /dev/null +++ b/cinder/zonemanager/drivers/cisco/cisco_fc_san_lookup_service.py @@ -0,0 +1,357 @@ +# (c) Copyright 2014 Cisco Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +import random + +from eventlet import greenthread +import six + +from cinder import exception +from cinder.i18n import _ +from cinder.openstack.common import excutils +from cinder.openstack.common import log as logging +from cinder.openstack.common import processutils +from cinder import ssh_utils +from cinder import utils +from cinder.zonemanager.drivers.cisco import cisco_fabric_opts as fabric_opts +import cinder.zonemanager.drivers.cisco.fc_zone_constants as ZoneConstant +from cinder.zonemanager.fc_san_lookup_service import FCSanLookupService +from cinder.zonemanager.utils import get_formatted_wwn + +LOG = logging.getLogger(__name__) + + +class CiscoFCSanLookupService(FCSanLookupService): + """The SAN lookup service that talks to Cisco switches. + + Version History: + 1.0.0 - Initial version + + """ + + VERSION = "1.0.0" + + def __init__(self, **kwargs): + """Initializing the client.""" + super(CiscoFCSanLookupService, self).__init__(self, **kwargs) + self.configuration = kwargs.get('configuration', None) + self.create_configuration() + + self.switch_user = "" + self.switch_port = "" + self.switch_pwd = "" + self.switch_ip = "" + self.sshpool = None + + self.fabric_configs = "" + + def create_configuration(self): + """Configuration specific to SAN context values.""" + config = self.configuration + + fabric_names = [x.strip() for x in 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. + # Cisco Zonesets require VSANs + if fabric_names: + self.fabric_configs = fabric_opts.load_fabric_configurations( + fabric_names) + + def get_device_mapping_from_network(self, + initiator_wwn_list, + target_wwn_list): + """Provides the initiator/target map for available SAN contexts. + + Looks up fcns database 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 + { + : { + '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 + + if not fabric_names: + raise exception.InvalidParameterValue( + err=_("Missing Fibre Channel SAN configuration " + "param - fc_fabric_names")) + + fabrics = [x.strip() for x in fabric_names.split(',')] + + LOG.debug("FC Fabric List: %s", fabrics) + if fabrics: + for t in target_wwn_list: + formatted_target_list.append(get_formatted_wwn(t)) + + for i in initiator_wwn_list: + formatted_initiator_list.append(get_formatted_wwn(i)) + + for fabric_name in fabrics: + self.switch_ip = self.fabric_configs[fabric_name].safe_get( + 'cisco_fc_fabric_address') + self.switch_user = self.fabric_configs[fabric_name].safe_get( + 'cisco_fc_fabric_user') + self.switch_pwd = self.fabric_configs[fabric_name].safe_get( + 'cisco_fc_fabric_password') + self.switch_port = self.fabric_configs[fabric_name].safe_get( + 'cisco_fc_fabric_port') + zoning_vsan = self.fabric_configs[fabric_name].safe_get( + 'cisco_zoning_vsan') + + # Get name server data from fabric and find the targets + # logged in + nsinfo = '' + LOG.debug("show fcns database for vsan %s", zoning_vsan) + nsinfo = self.get_nameserver_info(zoning_vsan) + + LOG.debug("Lookup service:fcnsdatabase-%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 fcns database" + " for vsan %s", zoning_vsan) + + 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 fcns database" + " for vsan %s", zoning_vsan) + + fabric_map = {'initiator_port_wwn_list': visible_initiators, + 'target_port_wwn_list': visible_targets + } + device_map[zoning_vsan] = fabric_map + LOG.debug("Device map for SAN context: %s", device_map) + return device_map + + def get_nameserver_info(self, fabric_vsan): + """Get fcns database info 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: + cmd = ZoneConstant.FCNS_SHOW + fabric_vsan + ' | no-more' + cli_output = self._get_switch_info(cmd) + except exception.FCSanLookupServiceException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed collecting show fcns database for" + " fabric")) + if cli_output: + nsinfo_list = self._parse_ns_output(cli_output) + + LOG.debug("Connector returning fcns info-%s", nsinfo_list) + return nsinfo_list + + def _get_switch_info(self, cmd_list): + stdout, stderr, sw_data = None, None, None + try: + stdout, stderr = self._run_ssh(cmd_list, True, 1) + LOG.debug("CLI output from ssh - output:%s", stdout) + 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': six.text_type(e)} + LOG.error(msg) + raise exception.CiscoZoningCliException(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 of device port wwn from ns info + """ + nsinfo_list = [] + for line in switch_data: + if not(" 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 fcns output string: %s") % line + LOG.error(msg) + raise exception.InvalidParameterValue(err=msg) + return nsinfo_list + + def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1): + + command = ' '.join(cmd_list) + + if not self.sshpool: + self.sshpool = ssh_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: + msg = _("Exception: %s") % six.text_type(e) + LOG.error(msg) + 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 where status return is expected. + + cmd_list is a list of commands, where each command is itself + a list of parameters. We use utils.check_ssh_injection to check each + command, but then join then with " ; " to form a single command. + """ + + # Check that each command is secure + for cmd in cmd_list: + utils.check_ssh_injection(cmd) + + # Combine into a single command. + command = ' ; '.join(map(lambda x: ' '.join(x), cmd_list)) + + if not self.sshpool: + self.sshpool = ssh_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) + 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: + msg = _("Exception: %s") % six.text_type(e) + LOG.error(msg) + last_exception = e + greenthread.sleep(random.randint(20, 500) / 100.0) + LOG.debug("Handling error case after SSH:%s", 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(): + msg = (_("Error executing command via ssh: %s") % + six.text_type(e)) + LOG.error(msg) + finally: + if stdin: + stdin.flush() + stdin.close() + if stdout: + stdout.close() + if stderr: + stderr.close() + + def cleanup(self): + self.sshpool = None diff --git a/cinder/zonemanager/drivers/cisco/cisco_fc_zone_client_cli.py b/cinder/zonemanager/drivers/cisco/cisco_fc_zone_client_cli.py new file mode 100644 index 000000000..7980708fd --- /dev/null +++ b/cinder/zonemanager/drivers/cisco/cisco_fc_zone_client_cli.py @@ -0,0 +1,483 @@ +# (c) Copyright 2014 Cisco Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +""" +Script to push the zone configuration to Cisco SAN switches. +""" +import random +import re + +from eventlet import greenthread +import six + +from cinder import exception +from cinder.i18n import _ +from cinder.openstack.common import excutils +from cinder.openstack.common import log as logging +from cinder.openstack.common import processutils +from cinder import ssh_utils +from cinder import utils +import cinder.zonemanager.drivers.cisco.fc_zone_constants as ZoneConstant + +LOG = logging.getLogger(__name__) + + +class CiscoFCZoneClientCLI(object): + """Cisco FC zone client cli implementation. + + OpenStack Fibre Channel zone client cli connector + to manage FC zoning in Cisco SAN fabrics. + + Version history: + 1.0 - Initial Cisco FC zone client cli + """ + + switch_ip = None + switch_port = '22' + switch_user = 'admin' + switch_pwd = 'none' + + def __init__(self, ipaddress, username, password, port, vsan): + """initializing the client.""" + self.switch_ip = ipaddress + self.switch_port = port + self.switch_user = username + self.switch_pwd = password + self.fabric_vsan = vsan + 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, self.fabric_vsan, + ' | no-more']) + except exception.CiscoZoningCliException: + 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: + # Split on non-word characters, + line_split = re.split('[\s\[\]]+', line) + if ZoneConstant.CFG_ZONESET in line_split: + # zoneset name [name] vsan [vsan] + zone_set_name = \ + line_split[line_split.index(ZoneConstant.CFG_ZONESET) + + 2] + continue + if ZoneConstant.CFG_ZONE in line_split: + # zone name [name] vsan [vsan] + zone_name = \ + line_split[line_split.index(ZoneConstant.CFG_ZONE) + 2] + zone[zone_name] = list() + continue + if ZoneConstant.CFG_ZONE_MEMBER in line_split: + # Examples: + # pwwn c0:50:76:05:15:9f:00:12 + # * fcid 0x1e01c0 [pwwn 50:05:07:68:02:20:48:04] [V7K_N1P2] + zone_member = \ + line_split[ + line_split.index(ZoneConstant.CFG_ZONE_MEMBER) + 1] + 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: + # In case 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) + exc_msg = _("Exception: %s") % six.text_type(ex) + LOG.exception(exc_msg) + raise exception.FCZoneDriverException(reason=msg) + + return zone_set + + def add_zones(self, zones, activate, fabric_vsan, active_zone_set, + zone_status): + """Add zone configuration. + + This method will add the zone configuration passed by user. + input params: + zones - zone names mapped to members and VSANs. + 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) + + LOG.debug("Active zone set:%s", active_zone_set) + zone_list = active_zone_set[ZoneConstant.CFG_ZONES] + LOG.debug("zone list:%s", zone_list) + LOG.debug("zone status:%s", zone_status) + + cfg_name = active_zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG] + + zone_cmds = [['conf'], + ['zoneset', 'name', cfg_name, 'vsan', fabric_vsan]] + + for zone in zones.keys(): + # if zone exists, its an update. Delete & insert + LOG.debug("Update call") + if zone in zone_list: + # Response from get_active_zone_set strips colons from WWPNs + current_zone = set(zone_list[zone]) + new_wwpns = map(lambda x: x.lower().replace(':', ''), + zones[zone]) + new_zone = set(new_wwpns) + + if current_zone != new_zone: + try: + self.delete_zones([zone], activate, fabric_vsan, + active_zone_set, zone_status) + except exception.CiscoZoningCliException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Deleting zone failed %s"), zone) + LOG.debug("Deleted Zone before insert : %s", zone) + + zone_cmds.append(['zone', 'name', zone]) + + for member in zones[zone]: + zone_cmds.append(['member', 'pwwn', member]) + + zone_cmds.append(['end']) + + try: + LOG.debug("Add zones: Config cmd to run:%s", zone_cmds) + self._ssh_execute(zone_cmds, True, 1) + + if activate: + self.activate_zoneset(cfg_name, fabric_vsan, zone_status) + self._cfg_save() + except Exception as e: + + msg = _("Creating and activating zone set failed: " + "(Zone set=%(zoneset)s error=%(err)s)." + ) % {'zoneset': cfg_name, 'err': six.text_type(e)} + LOG.error(msg) + raise exception.CiscoZoningCliException(reason=msg) + + def activate_zoneset(self, cfgname, fabric_vsan, zone_status): + """Method to Activate the zone config. Param cfgname - ZonesetName.""" + + LOG.debug("zone status:%s", zone_status) + + cmd_list = [['conf'], + ['zoneset', 'activate', 'name', cfgname, 'vsan', + self.fabric_vsan]] + if zone_status['mode'] == 'enhanced': + cmd_list.append(['zone', 'commit', 'vsan', fabric_vsan]) + + cmd_list.append(['end']) + + return self._ssh_execute(cmd_list, True, 1) + + def get_zoning_status(self): + """Return the zoning mode and session for a zoneset.""" + zone_status = {} + + try: + switch_data = self._get_switch_info( + [ZoneConstant.GET_ZONE_STATUS, self.fabric_vsan]) + except exception.CiscoZoningCliException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed getting zone status " + "from fabric %s"), self.switch_ip) + try: + for line in switch_data: + # Split on non-word characters, + line_split = re.split('[\s\[\]]+', line) + if 'mode:' in line_split: + # mode: + zone_status['mode'] = line_split[line_split.index('mode:') + + 1] + continue + if 'session:' in line_split: + # session: + zone_status['session'] = \ + line_split[line_split.index('session:') + 1] + continue + except Exception as ex: + # In case of parsing error here, it should be malformed cli output. + msg = _("Malformed zone status: (switch=%(switch)s " + "zone_config=%(zone_config)s)." + ) % {'switch': self.switch_ip, + 'zone_status': switch_data} + LOG.error(msg) + exc_msg = _("Exception: %s") % six.text_type(ex) + LOG.exception(exc_msg) + raise exception.FCZoneDriverException(reason=msg) + + return zone_status + + def delete_zones(self, zone_names, activate, fabric_vsan, active_zone_set, + zone_status): + """Delete zones from fabric. + + Method to delete the active zone config zones + + params zone_names: zoneNames separated by semicolon + params activate: True/False + """ + + LOG.debug("zone_names %s", zone_names) + active_zoneset_name = active_zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG] + + cmds = [['conf'], + ['zoneset', 'name', active_zoneset_name, 'vsan', + fabric_vsan]] + + try: + for zone in set(zone_names.split(';')): + cmds.append(['no', 'zone', 'name', zone]) + + cmds.append(['end']) + + LOG.debug("Delete zones: Config cmd to run:%s", cmds) + self._ssh_execute(cmds, True, 1) + + if activate: + self.activate_zoneset(active_zoneset_name, fabric_vsan, + zone_status) + self._cfg_save() + + except Exception as e: + msg = _("Deleting zones failed: (command=%(cmd)s error=%(err)s)." + ) % {'cmd': cmds, 'err': six.text_type(e)} + LOG.error(msg) + raise exception.CiscoZoningCliException(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 + + show fcns database + """ + cli_output = None + return_list = [] + try: + cli_output = self._get_switch_info([ZoneConstant.FCNS_SHOW, + self.fabric_vsan]) + except exception.CiscoZoningCliException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed collecting fcns database " + "info for fabric %s"), self.switch_ip) + + if (cli_output): + return_list = self._parse_ns_output(cli_output) + + LOG.info(_("Connector returning fcnsinfo-%s"), return_list) + + return return_list + + def _cfg_save(self): + cmd = ['copy', 'running-config', 'startup-config'] + self._run_ssh(cmd, True, 1) + + def _get_switch_info(self, cmd_list): + stdout, stderr, sw_data = None, None, None + try: + stdout, stderr = self._run_ssh(cmd_list, True, 1) + LOG.debug("CLI output from ssh - output:%s", stdout) + 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': six.text_type(e)} + LOG.error(msg) + raise exception.CiscoZoningCliException(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(" 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 show fcns database 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): + + command = ' '.join(cmd_list) + + if not self.sshpool: + self.sshpool = ssh_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: + msg = _("Exception: %s") % six.text_type(e) + LOG.error(msg) + 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 where status return is expected. + + cmd_list is a list of commands, where each command is itself + a list of parameters. We use utils.check_ssh_injection to check each + command, but then join then with " ; " to form a single command. + """ + + # Check that each command is secure + for cmd in cmd_list: + utils.check_ssh_injection(cmd) + + # Combine into a single command. + command = ' ; '.join(map(lambda x: ' '.join(x), cmd_list)) + + if not self.sshpool: + self.sshpool = ssh_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) + 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: + msg = _("Exception: %s") % six.text_type(e) + LOG.error(msg) + last_exception = e + greenthread.sleep(random.randint(20, 500) / 100.0) + LOG.debug("Handling error case after SSH:%s", 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(): + msg = (_("Error executing command via ssh: %s") % + six.text_type(e)) + LOG.error(msg) + finally: + if stdin: + stdin.flush() + stdin.close() + if stdout: + stdout.close() + if stderr: + stderr.close() + + def cleanup(self): + self.sshpool = None diff --git a/cinder/zonemanager/drivers/cisco/cisco_fc_zone_driver.py b/cinder/zonemanager/drivers/cisco/cisco_fc_zone_driver.py new file mode 100644 index 000000000..73c066549 --- /dev/null +++ b/cinder/zonemanager/drivers/cisco/cisco_fc_zone_driver.py @@ -0,0 +1,488 @@ +# (c) Copyright 2014 Cisco Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +""" +Cisco Zone Driver is responsible to manage access control using FC zoning +for Cisco 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 +import six + +from cinder import exception +from cinder.i18n import _ +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.cisco import cisco_fabric_opts as fabric_opts +from cinder.zonemanager.drivers.fc_zone_driver import FCZoneDriver +from cinder.zonemanager.utils import get_formatted_wwn + +LOG = logging.getLogger(__name__) + +cisco_opts = [ + cfg.StrOpt('cisco_sb_connector', + default='cinder.zonemanager.drivers.cisco' + '.cisco_fc_zone_client_cli.CiscoFCZoneClientCLI', + help='Southbound connector for zoning operation'), +] + +CONF = cfg.CONF +CONF.register_opts(cisco_opts, 'fc-zone-manager') + + +class CiscoFCZoneDriver(FCZoneDriver): + """Cisco FC zone driver implementation. + + OpenStack Fibre Channel zone driver to manage FC zoning in + Cisco SAN fabrics. + + Version history: + 1.0 - Initial Cisco FC zone driver + """ + + VERSION = "1.0.0" + + def __init__(self, **kwargs): + super(CiscoFCZoneDriver, self).__init__(**kwargs) + self.configuration = kwargs.get('configuration', None) + if self.configuration: + self.configuration.append_config_values(cisco_opts) + + # Adding a hack to handle parameters from super classes + # in case configured with multi backends. + fabric_names = self.configuration.safe_get('fc_fabric_names') + activate = self.configuration.safe_get('cisco_zone_activate') + prefix = self.configuration.safe_get('cisco_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('cisco_zone_activate', + default=True, + help='Indicates whether zone should ' + 'be activated or not')) + if not prefix: + base_san_opts.append( + cfg.StrOpt('cisco_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 = [x.strip() for x in self. + configuration.fc_fabric_names.split(',')] + + # There can be more than one SAN in the network and we need to + # get credentials for each SAN. + if fabric_names: + self.fabric_configs = fabric_opts.load_fabric_configurations( + fabric_names) + + @lockutils.synchronized('cisco', '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(_("CiscoFCZoneDriver - Add connection " + "for I-T map: %s"), initiator_target_map) + fabric_ip = self.fabric_configs[fabric].safe_get( + 'cisco_fc_fabric_address') + fabric_user = self.fabric_configs[fabric].safe_get( + 'cisco_fc_fabric_user') + fabric_pwd = self.fabric_configs[fabric].safe_get( + 'cisco_fc_fabric_password') + fabric_port = self.fabric_configs[fabric].safe_get( + 'cisco_fc_fabric_port') + zoning_policy = self.configuration.zoning_policy + zoning_policy_fab = self.fabric_configs[fabric].safe_get( + 'cisco_zoning_policy') + if zoning_policy_fab: + zoning_policy = zoning_policy_fab + + zoning_vsan = self.fabric_configs[fabric].safe_get('cisco_zoning_vsan') + + LOG.info(_("Zoning policy for Fabric %s"), zoning_policy) + + statusmap_from_fabric = self.get_zoning_status( + fabric_ip, fabric_user, fabric_pwd, fabric_port, zoning_vsan) + + if statusmap_from_fabric.get('session') == 'none': + + cfgmap_from_fabric = self.get_active_zone_set( + fabric_ip, fabric_user, fabric_pwd, fabric_port, zoning_vsan) + 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 = [get_formatted_wwn(initiator), + get_formatted_wwn(target)] + zone_name = (self. + configuration.cisco_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 exists. + LOG.info(_("Zone exists in I-T mode. " + "Skipping zone creation %s"), + zone_name) + elif zoning_policy == 'initiator': + zone_members = [get_formatted_wwn(initiator)] + for t in t_list: + target = t.lower() + zone_members.append(get_formatted_wwn(target)) + + zone_name = self.configuration.cisco_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: + conn = None + try: + conn = importutils.import_object( + self.configuration.cisco_sb_connector, + ipaddress=fabric_ip, + username=fabric_user, + password=fabric_pwd, + port=fabric_port, + vsan=zoning_vsan) + conn.add_zones( + zone_map, self.configuration.cisco_zone_activate, + zoning_vsan, cfgmap_from_fabric, + statusmap_from_fabric) + conn.cleanup() + except exception.CiscoZoningCliException as cisco_ex: + msg = _("Exception: %s") % six.text_type(cisco_ex) + raise exception.FCZoneDriverException(msg) + except Exception as e: + LOG.error(_("Exception: %s") % six.text_type(e)) + msg = (_("Failed to add zoning configuration %s") % + six.text_type(e)) + raise exception.FCZoneDriverException(msg) + LOG.debug("Zones added successfully: %s", zone_map) + else: + LOG.debug("Zoning session exists VSAN: %s", zoning_vsan) + + @lockutils.synchronized('cisco', '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(_("CiscoFCZoneDriver - Delete connection for I-T map: %s"), + initiator_target_map) + fabric_ip = self.fabric_configs[fabric].safe_get( + 'cisco_fc_fabric_address') + fabric_user = self.fabric_configs[fabric].safe_get( + 'cisco_fc_fabric_user') + fabric_pwd = self.fabric_configs[fabric].safe_get( + 'cisco_fc_fabric_password') + fabric_port = self.fabric_configs[fabric].safe_get( + 'cisco_fc_fabric_port') + zoning_policy = self.configuration.zoning_policy + zoning_policy_fab = self.fabric_configs[fabric].safe_get( + 'cisco_zoning_policy') + + if zoning_policy_fab: + zoning_policy = zoning_policy_fab + + zoning_vsan = self.fabric_configs[fabric].safe_get('cisco_zoning_vsan') + + LOG.info(_("Zoning policy for fabric %s"), zoning_policy) + + statusmap_from_fabric = self.get_zoning_status( + fabric_ip, fabric_user, fabric_pwd, fabric_port, zoning_vsan) + + if statusmap_from_fabric.get('session') == 'none': + cfgmap_from_fabric = self.get_active_zone_set( + fabric_ip, fabric_user, fabric_pwd, fabric_port, zoning_vsan) + + 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 = 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.cisco_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(get_formatted_wwn(target)) + + zone_name = self.configuration.cisco_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) + conn = None + try: + conn = importutils.import_object( + self.configuration.cisco_sb_connector, + ipaddress=fabric_ip, + username=fabric_user, + password=fabric_pwd, + port=fabric_port, + vsan=zoning_vsan) + # Update zone membership. + if zone_map: + conn.add_zones( + zone_map, self.configuration.cisco_zone_activate, + zoning_vsan, cfgmap_from_fabric, + statusmap_from_fabric) + # 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. + cisco_zone_activate, + zoning_vsan, cfgmap_from_fabric, + statusmap_from_fabric) + conn.cleanup() + except Exception as e: + msg = _("Exception: %s") % six.text_type(e) + LOG.error(msg) + msg = _("Failed to update or delete zoning configuration") + raise exception.FCZoneDriverException(msg) + LOG.debug("Zones deleted successfully: %s", zone_map) + else: + LOG.debug("Zoning session exists VSAN: %s", zoning_vsan) + + 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. + """ + formatted_target_list = [] + fabric_map = {} + fabrics = [x.strip() for x in 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(get_formatted_wwn(t.lower())) + LOG.debug("Formatted Target wwn List: %s", formatted_target_list) + for fabric_name in fabrics: + fabric_ip = self.fabric_configs[fabric_name].safe_get( + 'cisco_fc_fabric_address') + fabric_user = self.fabric_configs[fabric_name].safe_get( + 'cisco_fc_fabric_user') + fabric_pwd = self.fabric_configs[fabric_name].safe_get( + 'cisco_fc_fabric_password') + fabric_port = self.fabric_configs[fabric_name].safe_get( + 'cisco_fc_fabric_port') + zoning_vsan = self.fabric_configs[fabric_name].safe_get( + 'cisco_zoning_vsan') + + # Get name server data from fabric and get the targets + # logged in. + nsinfo = None + try: + conn = importutils.import_object( + self.configuration.cisco_sb_connector, + ipaddress=fabric_ip, + username=fabric_user, + password=fabric_pwd, port=fabric_port, + vsan=zoning_vsan) + nsinfo = conn.get_nameserver_info() + LOG.debug("show fcns database info from fabric:%s", nsinfo) + conn.cleanup() + except exception.CiscoZoningCliException as ex: + with excutils.save_and_reraise_exception(): + LOG.error(_("Error getting show fcns database " + "info: %s"), six.text_type(ex)) + except Exception as e: + msg = (_("Failed to get show fcns database info:%s") % + six.text_type(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] = six.text_type( + visible_targets[idx]).replace(':', '') + fabric_map[fabric_name] = visible_targets + else: + LOG.debug("No targets are in the fcns info 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, + zoning_vsan): + """Gets active zoneset config for vsan.""" + cfgmap = {} + conn = None + try: + LOG.debug("Southbound connector: %s", + self.configuration.cisco_sb_connector) + conn = importutils.import_object( + self.configuration.cisco_sb_connector, + ipaddress=fabric_ip, username=fabric_user, + password=fabric_pwd, port=fabric_port, vsan=zoning_vsan) + cfgmap = conn.get_active_zone_set() + conn.cleanup() + except Exception as e: + msg = (_("Failed to access active zoning configuration:%s") % + six.text_type(e)) + LOG.error(msg) + raise exception.FCZoneDriverException(msg) + LOG.debug("Active zone set from fabric: %s", cfgmap) + return cfgmap + + def get_zoning_status(self, fabric_ip, fabric_user, fabric_pwd, + fabric_port, zoning_vsan): + """Gets zoneset status and mode.""" + statusmap = {} + conn = None + try: + LOG.debug("Southbound connector: %s", + self.configuration.cisco_sb_connector) + conn = importutils.import_object( + self.configuration.cisco_sb_connector, + ipaddress=fabric_ip, username=fabric_user, + password=fabric_pwd, port=fabric_port, vsan=zoning_vsan) + statusmap = conn.get_zoning_status() + conn.cleanup() + except Exception as e: + msg = (_("Failed to access zoneset status:%s") % + six.text_type(e)) + LOG.error(msg) + raise exception.FCZoneDriverException(msg) + LOG.debug("Zoneset status from fabric: %s", statusmap) + return statusmap diff --git a/cinder/zonemanager/drivers/cisco/fc_zone_constants.py b/cinder/zonemanager/drivers/cisco/fc_zone_constants.py new file mode 100644 index 000000000..6e1a8755c --- /dev/null +++ b/cinder/zonemanager/drivers/cisco/fc_zone_constants.py @@ -0,0 +1,32 @@ +# (c) Copyright 2014 Cisco Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +""" +Common constants used by Cisco FC Zone Driver. +""" +ACTIVE_ZONE_CONFIG = 'active_zone_config' +CFG_ZONESET = 'zoneset' +CFG_ZONE = 'zone' +CFG_ZONE_MEMBER = 'pwwn' +CFG_ZONES = 'zones' + +""" +CLI Commands for FC zoning operations. +""" +GET_ACTIVE_ZONE_CFG = 'show zoneset active vsan ' +FCNS_SHOW = 'show fcns database vsan ' +GET_ZONE_STATUS = 'show zone status vsan ' diff --git a/cinder/zonemanager/utils.py b/cinder/zonemanager/utils.py index 13c91e3f8..ea12918d2 100644 --- a/cinder/zonemanager/utils.py +++ b/cinder/zonemanager/utils.py @@ -61,6 +61,15 @@ def create_lookup_service(): return None +def get_formatted_wwn(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() + + def AddFCZone(initialize_connection): """Decorator to add a FC Zone.""" def decorator(self, *args, **kwargs): diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 000bd4ac3..2c11dc91e 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -2224,6 +2224,37 @@ #principal_switch_wwn= +[CISCO_FABRIC_EXAMPLE] + +# +# Options defined in cinder.zonemanager.drivers.cisco.cisco_fabric_opts +# + +# Management IP of fabric (string value) +#cisco_fc_fabric_address= + +# Fabric user ID (string value) +#cisco_fc_fabric_user= + +# Password for user (string value) +#cisco_fc_fabric_password= + +# Connecting port (integer value) +#cisco_fc_fabric_port=22 + +# overridden zoning policy (string value) +#cisco_zoning_policy=initiator-target + +# overridden zoning activation state (boolean value) +#cisco_zone_activate=true + +# overridden zone name prefix (string value) +#cisco_zone_name_prefix= + +# VSAN of the Fabric (string value) +#cisco_zoning_vsan= + + [database] # @@ -2351,6 +2382,14 @@ #brcd_sb_connector=cinder.zonemanager.drivers.brocade.brcd_fc_zone_client_cli.BrcdFCZoneClientCLI +# +# Options defined in cinder.zonemanager.drivers.cisco.cisco_fc_zone_driver +# + +# Southbound connector for zoning operation (string value) +#cisco_sb_connector=cinder.zonemanager.drivers.cisco.cisco_fc_zone_client_cli.CiscoFCZoneClientCLI + + # # Options defined in cinder.zonemanager.fc_zone_manager #