From f6f25243d937d53ebfdfcf32a2679a60a20f7578 Mon Sep 17 00:00:00 2001 From: Patrick East Date: Tue, 1 Mar 2016 19:40:10 -0800 Subject: [PATCH] Revert "Remove Cisco FC Zone Manager Driver" MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This reverts commit 0f5c5d8cf7ae3623c69ca30e5dd1387f699bdc70. As a stop gap solution Pure Storage will be running the CI for the Cisco FCZM so that we don’t drop it for the release. Change-Id: If257edae46ea4cd8e512e600b9182e9f50cdf546 --- cinder/opts.py | 10 + .../test_cisco_fc_san_lookup_service.py | 149 +++++ .../test_cisco_fc_zone_client_cli.py | 303 ++++++++++ .../zonemanager/test_cisco_fc_zone_driver.py | 208 +++++++ .../zonemanager/test_cisco_lookup_service.py | 96 ++++ cinder/zonemanager/drivers/cisco/__init__.py | 0 .../drivers/cisco/cisco_fabric_opts.py | 56 ++ .../cisco/cisco_fc_san_lookup_service.py | 355 ++++++++++++ .../drivers/cisco/cisco_fc_zone_client_cli.py | 460 ++++++++++++++++ .../drivers/cisco/cisco_fc_zone_driver.py | 518 ++++++++++++++++++ .../drivers/cisco/fc_zone_constants.py | 32 ++ .../cisco-fczm-removed-502dfcba51acd1b4.yaml | 4 - 12 files changed, 2187 insertions(+), 4 deletions(-) create mode 100644 cinder/tests/unit/zonemanager/test_cisco_fc_san_lookup_service.py create mode 100644 cinder/tests/unit/zonemanager/test_cisco_fc_zone_client_cli.py create mode 100644 cinder/tests/unit/zonemanager/test_cisco_fc_zone_driver.py create mode 100644 cinder/tests/unit/zonemanager/test_cisco_lookup_service.py create mode 100644 cinder/zonemanager/drivers/cisco/__init__.py create mode 100644 cinder/zonemanager/drivers/cisco/cisco_fabric_opts.py create mode 100644 cinder/zonemanager/drivers/cisco/cisco_fc_san_lookup_service.py create mode 100644 cinder/zonemanager/drivers/cisco/cisco_fc_zone_client_cli.py create mode 100644 cinder/zonemanager/drivers/cisco/cisco_fc_zone_driver.py create mode 100644 cinder/zonemanager/drivers/cisco/fc_zone_constants.py delete mode 100644 releasenotes/notes/cisco-fczm-removed-502dfcba51acd1b4.yaml diff --git a/cinder/opts.py b/cinder/opts.py index d943dda22..bedea755c 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -168,6 +168,10 @@ from cinder.zonemanager.drivers.brocade import brcd_fabric_opts as \ cinder_zonemanager_drivers_brocade_brcdfabricopts from cinder.zonemanager.drivers.brocade import brcd_fc_zone_driver as \ cinder_zonemanager_drivers_brocade_brcdfczonedriver +from cinder.zonemanager.drivers.cisco import cisco_fabric_opts as \ + cinder_zonemanager_drivers_cisco_ciscofabricopts +from cinder.zonemanager.drivers.cisco import cisco_fc_zone_driver as \ + cinder_zonemanager_drivers_cisco_ciscofczonedriver from cinder.zonemanager import fc_zone_manager as \ cinder_zonemanager_fczonemanager @@ -178,6 +182,7 @@ def list_opts(): itertools.chain( cinder_zonemanager_fczonemanager.zone_manager_opts, cinder_zonemanager_drivers_brocade_brcdfczonedriver.brcd_opts, + cinder_zonemanager_drivers_cisco_ciscofczonedriver.cisco_opts, )), ('KEYMGR', itertools.chain( @@ -333,6 +338,11 @@ def list_opts(): cinder_volume_drivers_vzstorage.vzstorage_opts, cinder_volume_drivers_nfs.nfs_opts, )), + ('CISCO_FABRIC_EXAMPLE', + itertools.chain( + cinder_zonemanager_drivers_cisco_ciscofabricopts. + cisco_zone_opts, + )), ('BRCD_FABRIC_EXAMPLE', itertools.chain( cinder_zonemanager_drivers_brocade_brcdfabricopts. diff --git a/cinder/tests/unit/zonemanager/test_cisco_fc_san_lookup_service.py b/cinder/tests/unit/zonemanager/test_cisco_fc_san_lookup_service.py new file mode 100644 index 000000000..154285326 --- /dev/null +++ b/cinder/tests/unit/zonemanager/test_cisco_fc_san_lookup_service.py @@ -0,0 +1,149 @@ +# (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 +import cinder.zonemanager.drivers.cisco.fc_zone_constants as ZoneConstant +from cinder.zonemanager import utils as zm_utils + +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() + self.fabric_vsan = '304' + + # 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.PortOpt('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_to_verify, device_map) + + @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_expected, ns_info_list) + + 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(expected_wwn_list, return_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(zm_utils.get_formatted_wwn(wwn_list[0])) + self.assertEqual(expected_wwn_list, return_wwn_list) + + @mock.patch.object(cisco_lookup.CiscoFCSanLookupService, + '_run_ssh') + def test__get_switch_info(self, run_ssh_mock): + cmd_list = [ZoneConstant.FCNS_SHOW, self.fabric_vsan, + ' | no-more'] + nsshow_list = [nsshow] + run_ssh_mock.return_value = (Stream(nsshow), Stream()) + switch_data = self._get_switch_info(cmd_list) + self.assertEqual(nsshow_list, switch_data) + run_ssh_mock.assert_called_once_with(cmd_list, True, 1) + + +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/unit/zonemanager/test_cisco_fc_zone_client_cli.py b/cinder/tests/unit/zonemanager/test_cisco_fc_zone_client_cli.py new file mode 100644 index 000000000..b3e155ed5 --- /dev/null +++ b/cinder/tests/unit/zonemanager/test_cisco_fc_zone_client_cli.py @@ -0,0 +1,303 @@ +# (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.""" + +import time + +import mock +from oslo_concurrency import processutils +from six.moves import range + +from cinder import exception +from cinder import test +from cinder.zonemanager.drivers.cisco \ + import cisco_fc_zone_client_cli as cli +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'], + 'openstack10000012345678902001009876543210': + ['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(cli.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) + + @mock.patch.object(cli.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, active_zoneset_returned) + + @mock.patch.object(cli.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) + + @mock.patch.object(cli.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_basic, zoning_status_returned) + + @mock.patch.object(cli.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_enhanced_nosess, + zoning_status_returned) + + @mock.patch.object(cli.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_enhanced_sess, + zoning_status_returned) + + @mock.patch.object(cli.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_expected, ns_info_list) + + @mock.patch.object(cli.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) + + @mock.patch.object(cli.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) + + @mock.patch.object(cli.CiscoFCZoneClientCLI, '_run_ssh') + @mock.patch.object(time, 'sleep') + def test__cfg_save_with_retry(self, mock_sleep, run_ssh_mock): + cmd_list = ['copy', 'running-config', 'startup-config'] + run_ssh_mock.side_effect = [ + processutils.ProcessExecutionError, + ('', None) + ] + + self._cfg_save() + + self.assertEqual(2, run_ssh_mock.call_count) + run_ssh_mock.assert_has_calls([ + mock.call(cmd_list, True), + mock.call(cmd_list, True) + ]) + + @mock.patch.object(cli.CiscoFCZoneClientCLI, '_run_ssh') + @mock.patch.object(time, 'sleep') + def test__cfg_save_with_error(self, mock_sleep, run_ssh_mock): + cmd_list = ['copy', 'running-config', 'startup-config'] + run_ssh_mock.side_effect = processutils.ProcessExecutionError + + self.assertRaises(processutils.ProcessExecutionError, self._cfg_save) + + expected_num_calls = 5 + expected_calls = [] + for i in range(expected_num_calls): + expected_calls.append(mock.call(cmd_list, True)) + + self.assertEqual(expected_num_calls, run_ssh_mock.call_count) + run_ssh_mock.assert_has_calls(expected_calls) + + @mock.patch.object(cli.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(nsshow_list, switch_data) + run_ssh_mock.assert_called_once_with(cmd_list, True) + + @mock.patch.object(cli.CiscoFCZoneClientCLI, '_ssh_execute') + @mock.patch.object(cli.CiscoFCZoneClientCLI, '_cfg_save') + def test__add_zones_with_update(self, ssh_execute_mock, cfg_save_mock): + self.add_zones(new_zone, False, self.fabric_vsan, + active_zoneset_multiple_zones, + zoning_status_basic) + self.assertEqual(2, ssh_execute_mock.call_count) + self.assertEqual(2, cfg_save_mock.call_count) + + 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(expected_wwn_list, return_wwn_list) + + +class TestCiscoFCZoneClientCLISSH(test.TestCase): + + def setUp(self): + super(TestCiscoFCZoneClientCLISSH, self).setUp() + self.client = cli.CiscoFCZoneClientCLI(None, None, None, None, None) + self.client.sshpool = mock.MagicMock() + self.mock_ssh = self.client.sshpool.item().__enter__() + + @mock.patch('oslo_concurrency.processutils.ssh_execute') + def test__run_ssh(self, mock_execute): + mock_execute.return_value = 'ssh output' + ret = self.client._run_ssh(['cat', 'foo']) + self.assertEqual('ssh output', ret) + mock_execute.assert_called_once_with(self.mock_ssh, + 'cat foo', + check_exit_code=True) + + @mock.patch('oslo_concurrency.processutils.ssh_execute') + def test__run_ssh_with_error(self, mock_execute): + mock_execute.side_effect = processutils.ProcessExecutionError() + self.assertRaises(processutils.ProcessExecutionError, + self.client._run_ssh, + ['cat', 'foo']) + + +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/unit/zonemanager/test_cisco_fc_zone_driver.py b/cinder/tests/unit/zonemanager/test_cisco_fc_zone_driver.py new file mode 100644 index 000000000..42e5c555c --- /dev/null +++ b/cinder/tests/unit/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_concurrency import processutils +from oslo_config import cfg +from oslo_utils import importutils + +from cinder import exception +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.unit.zonemanager.' + 'test_cisco_fc_zone_driver.' + 'FakeCiscoFCZoneDriver') + configuration.cisco_sb_connector = ('cinder.tests.unit.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.unit.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/unit/zonemanager/test_cisco_lookup_service.py b/cinder/tests/unit/zonemanager/test_cisco_lookup_service.py new file mode 100644 index 000000000..678fd83da --- /dev/null +++ b/cinder/tests/unit/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 import fc_san_lookup_service as san_service + +_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(san_service.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.unit.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_to_verify, device_map) + + 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..524dcc1e9 --- /dev/null +++ b/cinder/zonemanager/drivers/cisco/cisco_fabric_opts.py @@ -0,0 +1,56 @@ +# (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 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.PortOpt('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', + help='overridden zone name prefix'), + cfg.StrOpt('cisco_zoning_vsan', + help='VSAN of the Fabric'), +] + +CONF = cfg.CONF +CONF.register_opts(cisco_zone_opts, group='CISCO_FABRIC_EXAMPLE') + + +def load_fabric_configurations(fabric_names): + fabric_configs = {} + for fabric_name in fabric_names: + config = configuration.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..77aaaa90b --- /dev/null +++ b/cinder/zonemanager/drivers/cisco/cisco_fc_san_lookup_service.py @@ -0,0 +1,355 @@ +# (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 +from oslo_concurrency import processutils +from oslo_log import log as logging +from oslo_utils import excutils +import six + +from cinder import exception +from cinder.i18n import _, _LE +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 zone_constant +from cinder.zonemanager import fc_san_lookup_service as fc_service +from cinder.zonemanager import utils as zm_utils + +LOG = logging.getLogger(__name__) + + +class CiscoFCSanLookupService(fc_service.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__(**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 + + 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(zm_utils.get_formatted_wwn(t)) + + for i in initiator_wwn_list: + formatted_initiator_list.append(zm_utils.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 = [x for x in nsinfo + if x in formatted_target_list] + visible_initiators = [x for x in nsinfo + if x in formatted_initiator_list] + + 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 = ([zone_constant.FCNS_SHOW, fabric_vsan, ' | no-more']) + cli_output = self._get_switch_info(cmd) + except exception.FCSanLookupServiceException: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("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(_LE("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..924dbe355 --- /dev/null +++ b/cinder/zonemanager/drivers/cisco/cisco_fc_zone_client_cli.py @@ -0,0 +1,460 @@ +# (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 +from oslo_concurrency import processutils +from oslo_log import log as logging +from oslo_utils import excutils +import six + +from cinder import exception +from cinder.i18n import _, _LE, _LI, _LW +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(_LE("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.error(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(_LE("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(_LE("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.error(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(_LE("Failed collecting fcns database " + "info for fabric %s"), self.switch_ip) + + if (cli_output): + return_list = self._parse_ns_output(cli_output) + + LOG.info(_LI("Connector returning fcnsinfo-%s"), return_list) + + return return_list + + @utils.retry(processutils.ProcessExecutionError, retries=5) + def _cfg_save(self): + cmd = ['copy', 'running-config', 'startup-config'] + self._run_ssh(cmd, True) + + def _get_switch_info(self, cmd_list): + stdout, stderr, sw_data = None, None, None + try: + stdout, stderr = self._run_ssh(cmd_list, True) + 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): + + 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) + try: + with self.sshpool.item() as ssh: + return processutils.ssh_execute( + ssh, + command, + check_exit_code=check_exit_code) + + except Exception: + with excutils.save_and_reraise_exception(): + LOG.warning(_LW("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) + 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.exception(_LE('Error executing SSH command.')) + 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: + with excutils.save_and_reraise_exception(): + LOG.exception(_LE("Error executing command via ssh.")) + 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..597d66b2b --- /dev/null +++ b/cinder/zonemanager/drivers/cisco/cisco_fc_zone_driver.py @@ -0,0 +1,518 @@ +# (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_concurrency import lockutils +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import excutils +from oslo_utils import importutils +import six +import string + +from cinder import exception +from cinder.i18n import _, _LE, _LI +from cinder.zonemanager.drivers.cisco import cisco_fabric_opts as fabric_opts +from cinder.zonemanager.drivers import driver_utils +from cinder.zonemanager.drivers import fc_zone_driver +from cinder.zonemanager import utils as zm_utils + +LOG = logging.getLogger(__name__) + +SUPPORTED_CHARS = string.ascii_letters + string.digits + '$' + '-' + '^' + '_' +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, group='fc-zone-manager') + + +class CiscoFCZoneDriver(fc_zone_driver.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 + 1.1 - Added friendly zone name support + """ + + VERSION = "1.1.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', + 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, host_name=None, + storage_system=None): + """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(_LI("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(_LI("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 = [ + zm_utils.get_formatted_wwn(initiator), + zm_utils.get_formatted_wwn(target)] + zone_name = ( + driver_utils.get_friendly_zone_name( + zoning_policy, + initiator, + target, + host_name, + storage_system, + self.configuration.cisco_zone_name_prefix, + SUPPORTED_CHARS)) + 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(_LI("Zone exists in I-T mode. " + "Skipping zone creation %s"), + zone_name) + elif zoning_policy == 'initiator': + zone_members = [ + zm_utils.get_formatted_wwn(initiator)] + for t in t_list: + target = t.lower() + zone_members.append( + zm_utils.get_formatted_wwn(target)) + + zone_name = ( + driver_utils.get_friendly_zone_name( + zoning_policy, + initiator, + target, + host_name, + storage_system, + self.configuration.cisco_zone_name_prefix, + SUPPORTED_CHARS)) + + 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(_LI("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: + msg = _("Failed to add zoning configuration.") + LOG.exception(msg) + 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, host_name=None, + storage_system=None): + """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(_LI("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(_LI("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 = zm_utils.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 = ( + driver_utils.get_friendly_zone_name( + zoning_policy, + initiator, + target, + host_name, + storage_system, + self.configuration.cisco_zone_name_prefix, + SUPPORTED_CHARS)) + 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( + zm_utils.get_formatted_wwn(target)) + + zone_name = driver_utils.get_friendly_zone_name( + zoning_policy, + initiator, + target, + host_name, + storage_system, + self.configuration.cisco_zone_name_prefix, + SUPPORTED_CHARS) + + 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(_LI("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: + msg = _("Failed to update or delete zoning configuration") + LOG.exception(msg) + 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( + zm_utils.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: + with excutils.save_and_reraise_exception(): + LOG.exception(_LE("Error getting show fcns database " + "info.")) + except Exception: + msg = _("Failed to get show fcns database info.") + LOG.exception(msg) + raise exception.FCZoneDriverException(msg) + visible_targets = filter( + lambda x: x in formatted_target_list, nsinfo) + + if visible_targets: + LOG.info(_LI("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: + msg = _("Failed to access active zoning configuration.") + LOG.exception(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: + msg = _("Failed to access zoneset status:%s") + LOG.exception(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/releasenotes/notes/cisco-fczm-removed-502dfcba51acd1b4.yaml b/releasenotes/notes/cisco-fczm-removed-502dfcba51acd1b4.yaml deleted file mode 100644 index 29949a5e5..000000000 --- a/releasenotes/notes/cisco-fczm-removed-502dfcba51acd1b4.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -upgrade: - - The Cisco FC Zone Manager driver is no longer supported - and not included in-tree. -- 2.45.2