diff --git a/cinder/exception.py b/cinder/exception.py index e9d78c55a..d0e6f04b1 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -689,3 +689,20 @@ class HPMSAConnectionError(HPMSAVolumeDriverException): class HPMSANotEnoughSpace(HPMSAVolumeDriverException): message = _("Not enough space on VDisk (%(vdisk)s)") + + +# Fibre Channel Zone Manager +class ZoneManagerException(CinderException): + message = _("Fibre Channel connection control failure: %(reason)s") + + +class FCZoneDriverException(CinderException): + message = _("Fibre Channel Zone operation failed: %(reason)s") + + +class FCSanLookupServiceException(CinderException): + message = _("Fibre Channel SAN Lookup failure: %(reason)s") + + +class BrocadeZoningCliException(CinderException): + message = _("Fibre Channel Zoning CLI error: %(reason)s") diff --git a/cinder/tests/test_brcd_fc_san_lookup_service.py b/cinder/tests/test_brcd_fc_san_lookup_service.py new file mode 100755 index 000000000..f9f7ba968 --- /dev/null +++ b/cinder/tests/test_brcd_fc_san_lookup_service.py @@ -0,0 +1,142 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +"""Unit tests for brcd fc san lookup service.""" + +import mock +import paramiko + +from oslo.config import cfg + +from cinder import exception +from cinder.openstack.common import log as logging +from cinder import test +from cinder.volume import configuration as conf +from cinder.zonemanager.drivers.brocade.brcd_fc_san_lookup_service \ + import BrcdFCSanLookupService +import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant +from mock import patch + +LOG = logging.getLogger(__name__) + +nsshow = '20:1a:00:05:1e:e8:e3:29' +switch_data = [' N 011a00;2,3;20:1a:00:05:1e:e8:e3:29;\ + 20:1a:00:05:1e:e8:e3:29;na'] +nsshow_data = ['10:00:8c:7c:ff:52:3b:01', '20:24:00:02:ac:00:0a:50'] +_device_map_to_verify = { + '100000051e55a100': { + 'initiator_port_wwn_list': ['10008c7cff523b01'], + 'target_port_wwn_list': ['20240002ac000a50']}} + + +class TestBrcdFCSanLookupService(BrcdFCSanLookupService, test.TestCase): + + def setUp(self): + super(TestBrcdFCSanLookupService, self).setUp() + self.client = paramiko.SSHClient() + self.configuration = conf.Configuration(None) + self.configuration.set_default('fc_fabric_names', 'BRCD_FAB_2') + self.create_configuration() + + # override some of the functions + def __init__(self, *args, **kwargs): + test.TestCase.__init__(self, *args, **kwargs) + + def create_configuration(self): + fc_fabric_opts = [] + fc_fabric_opts.append(cfg.StrOpt('fc_fabric_address_BRCD_FAB_2', + default='10.24.49.100', help='')) + fc_fabric_opts.append(cfg.StrOpt('fc_fabric_user_BRCD_FAB_2', + default='admin', help='')) + fc_fabric_opts.append(cfg.StrOpt('fc_fabric_password_BRCD_FAB_2', + default='password', help='', + secret=True)) + fc_fabric_opts.append(cfg.IntOpt('fc_fabric_port_BRCD_FAB_2', + default=22, help='')) + fc_fabric_opts.append(cfg.StrOpt('principal_switch_wwn_BRCD_FAB_2', + default='100000051e55a100', help='')) + self.configuration.append_config_values(fc_fabric_opts) + + @patch.object(BrcdFCSanLookupService, 'get_nameserver_info') + def test_get_device_mapping_from_network(self, get_nameserver_info_mock): + initiator_list = ['10008c7cff523b01'] + target_list = ['20240002ac000a50', '20240002ac000a40'] + with mock.patch.object(self.client, 'connect') \ + as client_connect_mock: + get_nameserver_info_mock.return_value = (nsshow_data) + device_map = self.get_device_mapping_from_network( + initiator_list, target_list) + self.assertDictMatch(device_map, _device_map_to_verify) + + @patch.object(BrcdFCSanLookupService, '_get_switch_data') + def test_get_nameserver_info(self, get_switch_data_mock): + ns_info_list = [] + ns_info_list_expected = ['20:1a:00:05:1e:e8:e3:29', + '20:1a:00:05:1e:e8:e3:29'] + get_switch_data_mock.return_value = (switch_data) + ns_info_list = self.get_nameserver_info() + self.assertEqual(ns_info_list, ns_info_list_expected) + + def test__get_switch_data(self): + cmd = ZoneConstant.NS_SHOW + + with mock.patch.object(self.client, 'exec_command') \ + as exec_command_mock: + exec_command_mock.return_value = (Stream(), + Stream(nsshow), + Stream()) + switch_data = self._get_switch_data(cmd) + self.assertEqual(switch_data, nsshow) + exec_command_mock.assert_called_once_with(cmd) + + def test__parse_ns_output(self): + invalid_switch_data = [' N 011a00;20:1a:00:05:1e:e8:e3:29'] + return_wwn_list = [] + expected_wwn_list = ['20:1a:00:05:1e:e8:e3:29'] + return_wwn_list = self._parse_ns_output(switch_data) + self.assertEqual(return_wwn_list, expected_wwn_list) + self.assertRaises(exception.InvalidParameterValue, + self._parse_ns_output, invalid_switch_data) + + def test_get_formatted_wwn(self): + wwn_list = ['10008c7cff523b01'] + return_wwn_list = [] + expected_wwn_list = ['10:00:8c:7c:ff:52:3b:01'] + return_wwn_list.append(self.get_formatted_wwn(wwn_list[0])) + self.assertEqual(return_wwn_list, expected_wwn_list) + + +class Channel(object): + def recv_exit_status(self): + return 0 + + +class Stream(object): + def __init__(self, buffer=''): + self.buffer = buffer + self.channel = Channel() + + def readlines(self): + return self.buffer + + def close(self): + pass + + def flush(self): + self.buffer = '' diff --git a/cinder/tests/test_brcd_fc_zone_client_cli.py b/cinder/tests/test_brcd_fc_zone_client_cli.py new file mode 100644 index 000000000..2fcf2e14c --- /dev/null +++ b/cinder/tests/test_brcd_fc_zone_client_cli.py @@ -0,0 +1,291 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +"""Unit tests for brcd fc zone client cli.""" + +import mock + +from cinder import exception +from cinder.openstack.common import log as logging +from cinder.openstack.common import processutils +from cinder import test +from cinder.zonemanager.drivers.brocade.brcd_fc_zone_client_cli \ + import BrcdFCZoneClientCLI +import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant +from mock import patch + +LOG = logging.getLogger(__name__) + +nsshow = '20:1a:00:05:1e:e8:e3:29' +switch_data = [' N 011a00;2,3;20:1a:00:05:1e:e8:e3:29;\ + 20:1a:00:05:1e:e8:e3:29;na', + ' Fabric Port Name: 20:1a:00:05:1e:e8:e3:29'] +cfgactvshow = ['Effective configuration:\n', + ' cfg:\tOpenStack_Cfg\t\n', + ' zone:\topenstack50060b0000c26604201900051ee8e329\t\n', + '\t\t50:06:0b:00:00:c2:66:04\n', + '\t\t20:19:00:05:1e:e8:e3:29\n'] +active_zoneset = { + 'zones': { + 'openstack50060b0000c26604201900051ee8e329': + ['50:06:0b:00:00:c2:66:04', '20:19:00:05:1e:e8:e3:29']}, + 'active_zone_config': 'OpenStack_Cfg'} +active_zoneset_multiple_zones = { + 'zones': { + 'openstack50060b0000c26604201900051ee8e329': + ['50:06:0b:00:00:c2:66:04', '20:19:00:05:1e:e8:e3:29'], + 'openstack50060b0000c26602201900051ee8e327': + ['50:06:0b:00:00:c2:66:02', '20:19:00:05:1e:e8:e3:27']}, + 'active_zone_config': 'OpenStack_Cfg'} +new_zone = {'openstack10000012345678902001009876543210': + ['10:00:00:12:34:56:78:90', '20:01:00:98:76:54:32:10']} +new_zones = {'openstack10000012345678902001009876543210': + ['10:00:00:12:34:56:78:90', '20:01:00:98:76:54:32:10'], + 'openstack10000011111111112001001111111111': + ['10:00:00:11:11:11:11:11', '20:01:00:11:11:11:11:11']} +zone_names_to_delete = 'openstack50060b0000c26604201900051ee8e329' +supported_firmware = ['Kernel: 2.6', 'Fabric OS: v7.0.1'] +unsupported_firmware = ['Fabric OS: v6.2.1'] + + +class TestBrcdFCZoneClientCLI(BrcdFCZoneClientCLI, test.TestCase): + + def setUp(self): + super(TestBrcdFCZoneClientCLI, self).setUp() + + # override some of the functions + def __init__(self, *args, **kwargs): + test.TestCase.__init__(self, *args, **kwargs) + + @patch.object(BrcdFCZoneClientCLI, '_get_switch_info') + def test_get_active_zone_set(self, get_switch_info_mock): + cmd_list = [ZoneConstant.GET_ACTIVE_ZONE_CFG] + get_switch_info_mock.return_value = cfgactvshow + active_zoneset_returned = self.get_active_zone_set() + get_switch_info_mock.assert_called_once_with(cmd_list) + self.assertDictMatch(active_zoneset_returned, active_zoneset) + + @patch.object(BrcdFCZoneClientCLI, '_run_ssh') + def test_get_active_zone_set_ssh_error(self, run_ssh_mock): + run_ssh_mock.side_effect = processutils.ProcessExecutionError + self.assertRaises(exception.BrocadeZoningCliException, + self.get_active_zone_set) + + @mock.patch.object(BrcdFCZoneClientCLI, 'get_active_zone_set') + @mock.patch.object(BrcdFCZoneClientCLI, 'apply_zone_change') + @mock.patch.object(BrcdFCZoneClientCLI, '_cfg_save') + def test_add_zones_new_zone_no_activate(self, get_active_zs_mock, + apply_zone_change_mock, + cfg_save_mock): + get_active_zs_mock.return_value = active_zoneset + self.add_zones(new_zones, False) + get_active_zs_mock.assert_called_once() + apply_zone_change_mock.assert_called_twice() + cfg_save_mock.assert_called_once() + + @mock.patch.object(BrcdFCZoneClientCLI, 'get_active_zone_set') + @mock.patch.object(BrcdFCZoneClientCLI, 'apply_zone_change') + @mock.patch.object(BrcdFCZoneClientCLI, '_cfg_save') + @mock.patch.object(BrcdFCZoneClientCLI, 'activate_zoneset') + def test_add_zones_new_zone_activate(self, get_active_zs_mock, + apply_zone_change_mock, + cfg_save_mock, + activate_zoneset_mock): + get_active_zs_mock.return_value = active_zoneset + self.add_zones(new_zone, True) + get_active_zs_mock.assert_called_once() + apply_zone_change_mock.assert_called_once() + cfg_save_mock.assert_called_once() + activate_zoneset_mock.assert_called_once() + + @mock.patch.object(BrcdFCZoneClientCLI, '_ssh_execute') + def test_activate_zoneset(self, ssh_execute_mock): + ssh_execute_mock.return_value = True + return_value = self.activate_zoneset('zoneset1') + self.assertTrue(return_value) + + @mock.patch.object(BrcdFCZoneClientCLI, '_ssh_execute') + def test_deactivate_zoneset(self, ssh_execute_mock): + ssh_execute_mock.return_value = True + return_value = self.deactivate_zoneset() + self.assertTrue(return_value) + + @mock.patch.object(BrcdFCZoneClientCLI, 'get_active_zone_set') + @mock.patch.object(BrcdFCZoneClientCLI, 'apply_zone_change') + @mock.patch.object(BrcdFCZoneClientCLI, '_cfg_save') + def test_delete_zones_activate_false(self, get_active_zs_mock, + apply_zone_change_mock, + cfg_save_mock): + get_active_zs_mock.return_value = active_zoneset_multiple_zones + with mock.patch.object(self, '_zone_delete') \ + as zone_delete_mock: + self.delete_zones(zone_names_to_delete, False) + get_active_zs_mock.assert_called_once() + apply_zone_change_mock.assert_called_once() + zone_delete_mock.assert_called_once_with(zone_names_to_delete) + cfg_save_mock.assert_called_once() + + @patch.object(BrcdFCZoneClientCLI, 'get_active_zone_set') + @patch.object(BrcdFCZoneClientCLI, 'apply_zone_change') + @patch.object(BrcdFCZoneClientCLI, '_cfg_save') + @patch.object(BrcdFCZoneClientCLI, 'activate_zoneset') + def test_delete_zones_activate_true(self, get_active_zs_mock, + apply_zone_change_mock, + cfg_save_mock, + activate_zs_mock): + get_active_zs_mock.return_value = active_zoneset_multiple_zones + with mock.patch.object(self, '_zone_delete') \ + as zone_delete_mock: + self.delete_zones(zone_names_to_delete, True) + get_active_zs_mock.assert_called_once() + apply_zone_change_mock.assert_called_once() + zone_delete_mock.assert_called_once_with(zone_names_to_delete) + cfg_save_mock.assert_called_once() + activate_zs_mock.assert_called_once() + + @patch.object(BrcdFCZoneClientCLI, '_get_switch_info') + def test_get_nameserver_info(self, get_switch_info_mock): + ns_info_list = [] + ns_info_list_expected = ['20:1a:00:05:1e:e8:e3:29', + '20:1a:00:05:1e:e8:e3:29'] + get_switch_info_mock.return_value = (switch_data) + ns_info_list = self.get_nameserver_info() + self.assertEqual(ns_info_list, ns_info_list_expected) + + @patch.object(BrcdFCZoneClientCLI, '_run_ssh') + def test_get_nameserver_info_ssh_error(self, run_ssh_mock): + run_ssh_mock.side_effect = processutils.ProcessExecutionError + self.assertRaises(exception.BrocadeZoningCliException, + self.get_nameserver_info) + + @patch.object(BrcdFCZoneClientCLI, '_ssh_execute') + def test__cfg_save(self, ssh_execute_mock): + cmd_list = [ZoneConstant.CFG_SAVE] + self._cfg_save() + ssh_execute_mock.assert_called_once_with(cmd_list, True, 1) + + @patch.object(BrcdFCZoneClientCLI, 'apply_zone_change') + def test__zone_delete(self, apply_zone_change_mock): + zone_name = 'testzone' + cmd_list = ['zonedelete', '"testzone"'] + self._zone_delete(zone_name) + apply_zone_change_mock.assert_called_once_with(cmd_list) + + @patch.object(BrcdFCZoneClientCLI, 'apply_zone_change') + def test__cfg_trans_abort(self, apply_zone_change_mock): + cmd_list = [ZoneConstant.CFG_ZONE_TRANS_ABORT] + with mock.patch.object(self, '_is_trans_abortable') \ + as is_trans_abortable_mock: + is_trans_abortable_mock.return_value = True + self._cfg_trans_abort() + is_trans_abortable_mock.assert_called_once() + apply_zone_change_mock.assert_called_once_with(cmd_list) + + @patch.object(BrcdFCZoneClientCLI, '_run_ssh') + def test__is_trans_abortable_true(self, run_ssh_mock): + cmd_list = [ZoneConstant.CFG_SHOW_TRANS] + run_ssh_mock.return_value = (Stream(ZoneConstant.TRANS_ABORTABLE), + None) + data = self._is_trans_abortable() + self.assertTrue(data) + run_ssh_mock.assert_called_once_with(cmd_list, True, 1) + + @patch.object(BrcdFCZoneClientCLI, '_run_ssh') + def test__is_trans_abortable_ssh_error(self, run_ssh_mock): + run_ssh_mock.return_value = (Stream(), Stream()) + self.assertRaises(exception.BrocadeZoningCliException, + self._is_trans_abortable) + + @patch.object(BrcdFCZoneClientCLI, '_run_ssh') + def test__is_trans_abortable_false(self, run_ssh_mock): + cmd_list = [ZoneConstant.CFG_SHOW_TRANS] + cfgtransshow = 'There is no outstanding zoning transaction' + run_ssh_mock.return_value = (Stream(cfgtransshow), None) + data = self._is_trans_abortable() + self.assertFalse(data) + run_ssh_mock.assert_called_once_with(cmd_list, True, 1) + + @patch.object(BrcdFCZoneClientCLI, '_run_ssh') + def test_apply_zone_change(self, run_ssh_mock): + cmd_list = [ZoneConstant.CFG_SAVE] + run_ssh_mock.return_value = (None, None) + self.apply_zone_change(cmd_list) + run_ssh_mock.assert_called_once_with(cmd_list, True, 1) + + @patch.object(BrcdFCZoneClientCLI, '_run_ssh') + def test__get_switch_info(self, run_ssh_mock): + cmd_list = [ZoneConstant.NS_SHOW] + nsshow_list = [nsshow] + run_ssh_mock.return_value = (Stream(nsshow), Stream()) + switch_data = self._get_switch_info(cmd_list) + self.assertEqual(switch_data, nsshow_list) + run_ssh_mock.assert_called_once_with(cmd_list, True, 1) + + def test__parse_ns_output(self): + invalid_switch_data = [' N 011a00;20:1a:00:05:1e:e8:e3:29'] + return_wwn_list = [] + expected_wwn_list = ['20:1a:00:05:1e:e8:e3:29'] + return_wwn_list = self._parse_ns_output(switch_data) + self.assertEqual(return_wwn_list, expected_wwn_list) + self.assertRaises(exception.InvalidParameterValue, + self._parse_ns_output, invalid_switch_data) + + @patch.object(BrcdFCZoneClientCLI, '_execute_shell_cmd') + def test_is_supported_firmware(self, exec_shell_cmd_mock): + exec_shell_cmd_mock.return_value = (supported_firmware, None) + self.assertTrue(self.is_supported_firmware()) + + @patch.object(BrcdFCZoneClientCLI, '_execute_shell_cmd') + def test_is_supported_firmware_invalid(self, exec_shell_cmd_mock): + exec_shell_cmd_mock.return_value = (unsupported_firmware, None) + self.assertFalse(self.is_supported_firmware()) + + @patch.object(BrcdFCZoneClientCLI, '_execute_shell_cmd') + def test_is_supported_firmware_no_ssh_response(self, exec_shell_cmd_mock): + exec_shell_cmd_mock.return_value = (None, Stream()) + self.assertFalse(self.is_supported_firmware()) + + @patch.object(BrcdFCZoneClientCLI, '_execute_shell_cmd') + def test_is_supported_firmware_ssh_error(self, exec_shell_cmd_mock): + exec_shell_cmd_mock.side_effect = processutils.ProcessExecutionError + self.assertRaises(exception.BrocadeZoningCliException, + self.is_supported_firmware) + + +class Channel(object): + def recv_exit_status(self): + return 0 + + +class Stream(object): + def __init__(self, buffer=''): + self.buffer = buffer + self.channel = Channel() + + def readlines(self): + return self.buffer + + def splitlines(self): + return self.buffer.splitlines() + + def close(self): + pass + + def flush(self): + self.buffer = '' diff --git a/cinder/tests/test_brcd_fc_zone_driver.py b/cinder/tests/test_brcd_fc_zone_driver.py new file mode 100644 index 000000000..e175688e0 --- /dev/null +++ b/cinder/tests/test_brcd_fc_zone_driver.py @@ -0,0 +1,233 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +"""Unit tests for Brocade fc zone driver.""" + +import paramiko + +from oslo.config import cfg + +from cinder import exception +from cinder.openstack.common import importutils +from cinder.openstack.common import log as logging +from cinder import test +from cinder.volume import configuration as conf + +LOG = logging.getLogger(__name__) + +_active_cfg_before_add = {} +_active_cfg_before_delete = { + 'zones': { + 'openstack10008c7cff523b0120240002ac000a50': ( + ['10:00:8c:7c:ff:52:3b:01', + '20:24:00:02:ac:00:0a:50']), 't_zone': ['1,0']}, + 'active_zone_config': 'cfg1'} +_activate = True +_zone_name = 'openstack10008c7cff523b0120240002ac000a50' +_target_ns_map = {'100000051e55a100': ['20240002ac000a50']} +_initiator_ns_map = {'100000051e55a100': ['10008c7cff523b01']} +_zone_map_to_add = {'openstack10008c7cff523b0120240002ac000a50': ( + ['10:00:8c:7c:ff:52:3b:01', '20:24:00:02:ac:00:0a:50'])} + +_initiator_target_map = {'10008c7cff523b01': ['20240002ac000a50']} +_device_map_to_verify = { + '100000051e55a100': { + 'initiator_port_wwn_list': [ + '10008c7cff523b01'], 'target_port_wwn_list': ['20240002ac000a50']}} +_fabric_wwn = '100000051e55a100' + + +class BrcdFcZoneDriverBaseTest(object): + + def setup_config(self, is_normal, mode): + fc_test_opts = [ + cfg.StrOpt('fc_fabric_address_BRCD_FAB_1', default='10.24.48.213', + help='FC Fabric names'), + ] + configuration = conf.Configuration(fc_test_opts) + # fill up config + configuration.zoning_mode = 'fabric' + configuration.zone_driver = ('cinder.tests.test_brcd_fc_zone_driver' + '.FakeBrcdFCZoneDriver') + configuration.brcd_sb_connector = ('cinder.tests.' + 'test_brcd_fc_zone_driver' + '.FakeBrcdFCZoneClientCLI') + configuration.zoning_policy = 'initiator-target' + configuration.zone_activate = True + configuration.zone_name_prefix = 'openstack' + configuration.fc_san_lookup_service = ('cinder.tests.' + 'test_brcd_fc_zone_driver.' + 'FakeBrcdFCSanLookupService') + + configuration.fc_fabric_names = 'BRCD_FAB_1' + configuration.fc_fabric_address_BRCD_FAB_1 = '10.24.48.213' + if (is_normal): + configuration.fc_fabric_user_BRCD_FAB_1 = 'admin' + else: + configuration.fc_fabric_user_BRCD_FAB_1 = 'invaliduser' + configuration.fc_fabric_password_BRCD_FAB_1 = 'password' + + if (mode == 1): + configuration.zoning_policy_BRCD_FAB_1 = 'initiator-target' + elif (mode == 2): + configuration.zoning_policy_BRCD_FAB_1 = 'initiator' + else: + configuration.zoning_policy_BRCD_FAB_1 = 'initiator-target' + configuration.zone_activate_BRCD_FAB_1 = True + configuration.zone_name_prefix_BRCD_FAB_1 = 'openstack_fab1' + configuration.principal_switch_wwn_BRCD_FAB_1 = '100000051e55a100' + return configuration + + +class TestBrcdFcZoneDriver(BrcdFcZoneDriverBaseTest, test.TestCase): + + def setUp(self): + super(TestBrcdFcZoneDriver, self).setUp() + # setup config for normal flow + self.setup_driver(self.setup_config(True, 1)) + GlobalVars._zone_state = [] + + def setup_driver(self, config): + self.driver = importutils.import_object( + 'cinder.zonemanager.drivers.brocade.brcd_fc_zone_driver' + '.BrcdFCZoneDriver', configuration=config) + + def fake_get_active_zone_set(self, fabric_ip, fabric_user, fabric_pwd): + return GlobalVars._active_cfg + + def fake_get_san_context(self, target_wwn_list): + fabric_map = {} + return fabric_map + + def test_add_connection(self): + """Normal flow for i-t mode.""" + GlobalVars._active_cfg = _active_cfg_before_add + GlobalVars._is_normal_test = True + GlobalVars._zone_state = [] + LOG.info(_("In Add GlobalVars._active_cfg:" + " %s"), GlobalVars._active_cfg) + LOG.info(_("In Add GlobalVars._is_normal_test: " + "%s"), GlobalVars._is_normal_test) + LOG.info(_("In Add GlobalVars._zone_state:" + " %s"), GlobalVars._zone_state) + self.driver.add_connection('BRCD_FAB_1', _initiator_target_map) + self.assertTrue(_zone_name in GlobalVars._zone_state) + + def test_delete_connection(self): + GlobalVars._is_normal_test = True + GlobalVars._active_cfg = _active_cfg_before_delete + self.driver.delete_connection( + 'BRCD_FAB_1', _initiator_target_map) + self.assertFalse(_zone_name in GlobalVars._zone_state) + + def test_add_connection_for_initiator_mode(self): + """Normal flow for i mode.""" + GlobalVars._is_normal_test = True + GlobalVars._active_cfg = _active_cfg_before_add + self.setup_driver(self.setup_config(True, 2)) + self.driver.add_connection('BRCD_FAB_1', _initiator_target_map) + self.assertTrue(_zone_name in GlobalVars._zone_state) + + def test_delete_connection_for_initiator_mode(self): + GlobalVars._is_normal_test = True + GlobalVars._active_cfg = _active_cfg_before_delete + self.setup_driver(self.setup_config(True, 2)) + self.driver.delete_connection( + 'BRCD_FAB_1', _initiator_target_map) + self.assertFalse(_zone_name in GlobalVars._zone_state) + + def test_add_connection_for_invalid_fabric(self): + """Test abnormal flows.""" + GlobalVars._is_normal_test = True + GlobalVars._active_cfg = _active_cfg_before_add + GlobalVars._is_normal_test = False + self.setup_driver(self.setup_config(False, 1)) + self.assertRaises(exception.FCZoneDriverException, + self.driver.add_connection, + 'BRCD_FAB_1', + _initiator_target_map) + + def test_delete_connection_for_invalid_fabric(self): + GlobalVars._active_cfg = _active_cfg_before_delete + GlobalVars._is_normal_test = False + self.setup_driver(self.setup_config(False, 1)) + self.assertRaises(exception.FCZoneDriverException, + self.driver.delete_connection, + 'BRCD_FAB_1', + _initiator_target_map) + + +class FakeBrcdFCZoneClientCLI(object): + def __init__(self, ipaddress, username, password, port): + LOG.info(_("User: %s"), username) + LOG.info(_("_zone_state: %s"), GlobalVars._zone_state) + if not GlobalVars._is_normal_test: + raise paramiko.SSHException("Unable to connect to fabric") + + def get_active_zone_set(self): + LOG.debug(_("Inside get_active_zone_set %s"), GlobalVars._active_cfg) + return GlobalVars._active_cfg + + def add_zones(self, zones, isActivate): + GlobalVars._zone_state.extend(zones.keys()) + + def delete_zones(self, zone_names, isActivate): + zone_list = zone_names.split(';') + GlobalVars._zone_state = [ + x for x in GlobalVars._zone_state if x not in zone_list] + + def is_supported_firmware(self): + return True + + def get_nameserver_info(self): + return _target_ns_map + + def close_connection(self): + pass + + def cleanup(self): + pass + + +class FakeBrcdFCSanLookupService(object): + def get_device_mapping_from_network(self, + initiator_wwn_list, + target_wwn_list): + device_map = {} + initiators = [] + targets = [] + for i in initiator_wwn_list: + if (i in _initiator_ns_map[_fabric_wwn]): + initiators.append(i) + for t in target_wwn_list: + if (t in _target_ns_map[_fabric_wwn]): + targets.append(t) + device_map[_fabric_wwn] = { + 'initiator_port_wwn_list': initiators, + 'target_port_wwn_list': targets} + return device_map + + +class GlobalVars(object): + global _active_cfg + _active_cfg = {} + global _zone_state + _zone_state = list() + global _is_normal_test + _is_normal_test = True diff --git a/cinder/tests/test_brcd_lookup_service.py b/cinder/tests/test_brcd_lookup_service.py new file mode 100644 index 000000000..8a44b6d4d --- /dev/null +++ b/cinder/tests/test_brcd_lookup_service.py @@ -0,0 +1,100 @@ +# (c) Copyright 2013 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +"""Unit tests for fc san lookup service.""" + +from cinder import exception +from cinder.openstack.common import log as logging +from cinder import test +from cinder.volume import configuration as conf +from cinder.zonemanager.fc_san_lookup_service import FCSanLookupService + +LOG = logging.getLogger(__name__) + +_target_ns_map = {'100000051e55a100': ['20240002ac000a50']} +_initiator_ns_map = {'100000051e55a100': ['10008c7cff523b01']} +_device_map_to_verify = { + '100000051e55a100': { + 'initiator_port_wwn_list': [ + '10008c7cff523b01'], 'target_port_wwn_list': ['20240002ac000a50']}} +_fabric_wwn = '100000051e55a100' + + +class TestFCSanLookupService(FCSanLookupService, test.TestCase): + + def setUp(self): + super(TestFCSanLookupService, self).setUp() + self.configuration = self.setup_config() + + # override some of the functions + def __init__(self, *args, **kwargs): + test.TestCase.__init__(self, *args, **kwargs) + + def setup_config(self): + configuration = conf.Configuration(None) + # fill up config + configuration.fc_san_lookup_service = ( + 'cinder.tests.test_brcd_lookup_service.FakeBrcdFCSanLookupService') + return configuration + + def test_get_device_mapping_from_network(self): + GlobalParams._is_normal_test = True + initiator_list = ['10008c7cff523b01'] + target_list = ['20240002ac000a50', '20240002ac000a40'] + device_map = self.get_device_mapping_from_network( + initiator_list, target_list) + self.assertDictMatch(device_map, _device_map_to_verify) + + def test_get_device_mapping_from_network_for_invalid_config(self): + GlobalParams._is_normal_test = False + initiator_list = ['10008c7cff523b01'] + target_list = ['20240002ac000a50', '20240002ac000a40'] + self.assertRaises(exception.FCSanLookupServiceException, + self.get_device_mapping_from_network, + initiator_list, target_list) + + +class FakeBrcdFCSanLookupService(object): + + def __init__(self, **kwargs): + pass + + def get_device_mapping_from_network(self, + initiator_wwn_list, + target_wwn_list): + if not GlobalParams._is_normal_test: + raise exception.FCSanLookupServiceException("Error") + device_map = {} + initiators = [] + targets = [] + for i in initiator_wwn_list: + if (i in _initiator_ns_map[_fabric_wwn]): + initiators.append(i) + for t in target_wwn_list: + if (t in _target_ns_map[_fabric_wwn]): + targets.append(t) + device_map[_fabric_wwn] = { + 'initiator_port_wwn_list': initiators, + 'target_port_wwn_list': targets} + return device_map + + +class GlobalParams(object): + global _is_normal_test + _is_normal_test = True diff --git a/cinder/tests/test_fc_zone_manager.py b/cinder/tests/test_fc_zone_manager.py new file mode 100644 index 000000000..909e80069 --- /dev/null +++ b/cinder/tests/test_fc_zone_manager.py @@ -0,0 +1,78 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +"""Unit tests for FC Zone Manager.""" + +import mock + +from cinder import exception +from cinder import test +from cinder.volume import configuration as conf +from cinder.zonemanager.drivers.fc_zone_driver import FCZoneDriver +from cinder.zonemanager.fc_zone_manager import ZoneManager +from mock import Mock + +fabric_name = 'BRCD_FAB_3' +init_target_map = {'10008c7cff523b01': ['20240002ac000a50']} +fabric_map = {'BRCD_FAB_3': ['20240002ac000a50']} +target_list = ['20240002ac000a50'] + + +class TestFCZoneManager(ZoneManager, test.TestCase): + + def setUp(self): + super(TestFCZoneManager, self).setUp() + self.configuration = conf.Configuration(None) + self.configuration.set_default('fc_fabric_names', fabric_name) + self.driver = Mock(FCZoneDriver) + + def __init__(self, *args, **kwargs): + test.TestCase.__init__(self, *args, **kwargs) + + def test_add_connection(self): + with mock.patch.object(self.driver, 'add_connection')\ + as add_connection_mock: + self.driver.get_san_context.return_value = fabric_map + self.add_connection(init_target_map) + self.driver.get_san_context.assert_called_once(target_list) + add_connection_mock.assert_called_once_with(fabric_name, + init_target_map) + + def test_add_connection_error(self): + with mock.patch.object(self.driver, 'add_connection')\ + as add_connection_mock: + add_connection_mock.side_effect = exception.FCZoneDriverException + self.assertRaises(exception.ZoneManagerException, + self.add_connection, init_target_map) + + def test_delete_connection(self): + with mock.patch.object(self.driver, 'delete_connection')\ + as delete_connection_mock: + self.driver.get_san_context.return_value = fabric_map + self.delete_connection(init_target_map) + self.driver.get_san_context.assert_called_once_with(target_list) + delete_connection_mock.assert_called_once_with(fabric_name, + init_target_map) + + def test_delete_connection_error(self): + with mock.patch.object(self.driver, 'delete_connection')\ + as del_connection_mock: + del_connection_mock.side_effect = exception.FCZoneDriverException + self.assertRaises(exception.ZoneManagerException, + self.delete_connection, init_target_map) diff --git a/cinder/tests/test_volume_manager_fc.py b/cinder/tests/test_volume_manager_fc.py new file mode 100644 index 000000000..c55939465 --- /dev/null +++ b/cinder/tests/test_volume_manager_fc.py @@ -0,0 +1,148 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +"""Unit tests for Volume Manager.""" + +import mock + +from cinder import exception +from cinder import test +from cinder import utils +from cinder.volume import configuration as conf +from cinder.volume.driver import VolumeDriver +from cinder.volume.manager import VolumeManager +from cinder.zonemanager.fc_zone_manager import ZoneManager +from mock import Mock +from mock import patch + +init_target_map = {'10008c7cff523b01': ['20240002ac000a50']} +conn_info = { + 'driver_volume_type': 'fibre_channel', + 'data': { + 'target_discovered': True, + 'target_lun': 1, + 'target_wwn': '20240002ac000a50', + 'initiator_target_map': { + '10008c7cff523b01': ['20240002ac000a50'] + } + } +} +conn_info_no_init_target_map = { + 'driver_volume_type': 'fibre_channel', + 'data': { + 'target_discovered': True, + 'target_lun': 1, + 'target_wwn': '20240002ac000a50', + } +} + + +class TestVolumeManager(VolumeManager, test.TestCase): + + def setUp(self): + super(TestVolumeManager, self).setUp() + self.configuration = conf.Configuration(None) + self.configuration.set_default('fc_fabric_names', 'BRCD_FAB_4') + self.configuration.zoning_mode = 'fabric' + self.driver = Mock(VolumeDriver) + self.driver.initialize_connection.return_value = conn_info + self.driver.terminate_connection.return_value = conn_info + self.driver.create_export.return_value = None + self.db = Mock() + self.db.volume_get.return_value = {'volume_type_id': None} + self.db.volume_admin_metadata_get.return_value = {} + self.context_mock = Mock() + self.context_mock.elevated.return_value = None + + def __init__(self, *args, **kwargs): + test.TestCase.__init__(self, *args, **kwargs) + + @patch.object(utils, 'require_driver_initialized') + def test_initialize_connection_voltype_fc_mode_fabric(self, + utils_mock): + utils_mock.return_value = True + with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\ + as add_del_conn_mock: + self.initialize_connection(self.context_mock, None, None) + add_del_conn_mock.assert_called_once_with(conn_info, 1) + + @patch.object(utils, 'require_driver_initialized') + def test_initialize_connection_voltype_fc_mode_none(self, + utils_mock): + utils_mock.return_value = True + with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\ + as add_del_conn_mock: + self.configuration.zoning_mode = 'none' + self.initialize_connection(self.context_mock, None, None) + assert not add_del_conn_mock.called + + def test_terminate_connection_exception(self): + with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\ + as add_del_conn_mock: + add_del_conn_mock.side_effect = exception.ZoneManagerException + self.assertRaises(exception.VolumeBackendAPIException, + self.terminate_connection, None, None, None, + False) + + @patch.object(utils, 'require_driver_initialized') + def test_terminate_connection_voltype_fc_mode_fabric(self, + utils_mock): + utils_mock.return_value = True + with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\ + as add_del_conn_mock: + self.terminate_connection(None, None, None, False) + add_del_conn_mock.assert_called_once_with(conn_info, 0) + + @patch.object(utils, 'require_driver_initialized') + def test_terminate_connection_mode_none(self, + utils_mock): + utils_mock.return_value = True + with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\ + as add_del_conn_mock: + self.configuration.zoning_mode = 'none' + self.terminate_connection(None, None, None, False) + assert not add_del_conn_mock.called + + @patch.object(utils, 'require_driver_initialized') + def test_terminate_connection_conn_info_none(self, + utils_mock): + utils_mock.return_value = True + self.driver.terminate_connection.return_value = None + with mock.patch.object(VolumeManager, '_add_or_delete_fc_connection')\ + as add_del_conn_mock: + self.terminate_connection(None, None, None, False) + assert not add_del_conn_mock.called + + @patch.object(ZoneManager, 'add_connection') + def test__add_or_delete_connection_add(self, + add_connection_mock): + self._add_or_delete_fc_connection(conn_info, 1) + add_connection_mock.assert_called_once_with(init_target_map) + + @patch.object(ZoneManager, 'delete_connection') + def test__add_or_delete_connection_delete(self, + delete_connection_mock): + self._add_or_delete_fc_connection(conn_info, 0) + delete_connection_mock.assert_called_once_with(init_target_map) + + @patch.object(ZoneManager, 'delete_connection') + def test__add_or_delete_connection_no_init_target_map(self, + del_conn_mock): + self._add_or_delete_fc_connection(conn_info_no_init_target_map, 0) + assert not del_conn_mock.called diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index 3a96c5817..5960d6f40 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -58,6 +58,7 @@ from cinder.volume.flows.manager import create_volume from cinder.volume import rpcapi as volume_rpcapi from cinder.volume import utils as volume_utils from cinder.volume import volume_types +from cinder.zonemanager.fc_zone_manager import ZoneManager from eventlet.greenpool import GreenPool @@ -77,6 +78,9 @@ volume_manager_opts = [ default=False, help='Offload pending volume delete during ' 'volume service startup'), + cfg.StrOpt('zoning_mode', + default='none', + help='FC Zoning mode configured'), ] CONF = cfg.CONF @@ -821,6 +825,13 @@ class VolumeManager(manager.SchedulerDependentManager): if volume_metadata.get('readonly') == 'True' else 'rw') conn_info['data']['access_mode'] = access_mode + # NOTE(skolathur): If volume_type is fibre_channel, invoke + # FCZoneManager to add access control via FC zoning. + vol_type = conn_info.get('driver_volume_type', None) + mode = self.configuration.zoning_mode + LOG.debug(_("Zoning Mode: %s"), mode) + if vol_type == 'fibre_channel' and mode == 'fabric': + self._add_or_delete_fc_connection(conn_info, 1) return conn_info def terminate_connection(self, context, volume_id, connector, force=False): @@ -835,8 +846,17 @@ class VolumeManager(manager.SchedulerDependentManager): volume_ref = self.db.volume_get(context, volume_id) try: - self.driver.terminate_connection(volume_ref, - connector, force=force) + conn_info = self.driver.terminate_connection(volume_ref, + connector, + force=force) + # NOTE(skolathur): If volume_type is fibre_channel, invoke + # FCZoneManager to remove access control via FC zoning. + if conn_info: + vol_type = conn_info.get('driver_volume_type', None) + mode = self.configuration.zoning_mode + LOG.debug(_("Zoning Mode: %s"), mode) + if vol_type == 'fibre_channel' and mode == 'fabric': + self._add_or_delete_fc_connection(conn_info, 0) except Exception as err: err_msg = (_('Unable to terminate volume connection: %(err)s') % {'err': str(err)}) @@ -1246,3 +1266,37 @@ class VolumeManager(manager.SchedulerDependentManager): if new_reservations: QUOTAS.commit(context, new_reservations, project_id=project_id) self.publish_service_capabilities(context) + + def _add_or_delete_fc_connection(self, conn_info, zone_op): + """Add or delete connection control to fibre channel network. + + In case of fibre channel, when zoning mode is set as fabric + ZoneManager is invoked to apply FC zoning configuration to the network + using initiator and target WWNs used for attach/detach. + + params conn_info: connector passed by volume driver after + initialize_connection or terminate_connection. + params zone_op: Indicates if it is a zone add or delete operation + zone_op=0 for delete connection and 1 for add connection + """ + _initiator_target_map = None + if 'initiator_target_map' in conn_info['data']: + _initiator_target_map = conn_info['data']['initiator_target_map'] + LOG.debug(_("Initiator Target map:%s"), _initiator_target_map) + # NOTE(skolathur): Invoke Zonemanager to handle automated FC zone + # management when vol_type is fibre_channel and zoning_mode is fabric + # Initiator_target map associating each initiator WWN to one or more + # target WWN is passed to ZoneManager to add or update zone config. + LOG.debug(_("Zoning op: %s"), zone_op) + if _initiator_target_map is not None: + kwargs = {'driver_volume_type': 'fibre_channel', + 'configuration': self.configuration} + zonemanager = ZoneManager(**kwargs) + try: + if zone_op == 1: + zonemanager.add_connection(_initiator_target_map) + elif zone_op == 0: + zonemanager.delete_connection(_initiator_target_map) + except exception.ZoneManagerException as e: + with excutils.save_and_reraise_exception(): + LOG.error(str(e)) diff --git a/cinder/zonemanager/__init__.py b/cinder/zonemanager/__init__.py new file mode 100644 index 000000000..4a9630ee1 --- /dev/null +++ b/cinder/zonemanager/__init__.py @@ -0,0 +1,27 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +""" +:mod:`cinder.zonemanager` -- FC Zone manager +===================================================== + +.. automodule:: cinder.zonemanager + :platform: Unix + :synopsis: Module containing all the FC Zone Manager classes +""" diff --git a/cinder/zonemanager/drivers/__init__.py b/cinder/zonemanager/drivers/__init__.py new file mode 100644 index 000000000..499708883 --- /dev/null +++ b/cinder/zonemanager/drivers/__init__.py @@ -0,0 +1,27 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +""" +:mod:`cinder.zonemanager.driver` -- FC Zone Drivers +===================================================== + +.. automodule:: cinder.zonemanager.driver + :platform: Unix + :synopsis: Module containing all the FC Zone drivers. +""" diff --git a/cinder/zonemanager/drivers/brocade/__init__.py b/cinder/zonemanager/drivers/brocade/__init__.py new file mode 100644 index 000000000..bfe856a97 --- /dev/null +++ b/cinder/zonemanager/drivers/brocade/__init__.py @@ -0,0 +1,27 @@ +# (c) Copyright 2013 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +""" +:mod:`cinder.zonemanager.driver.brocade` -- Brocade FC Zone Drivers +===================================================== + +.. automodule:: cinder.zonemanager.driver.brocade + :platform: Unix + :synopsis: Module containing all the Brocade FC Zone drivers. +""" diff --git a/cinder/zonemanager/drivers/brocade/brcd_fc_san_lookup_service.py b/cinder/zonemanager/drivers/brocade/brcd_fc_san_lookup_service.py new file mode 100644 index 000000000..211eaf5c6 --- /dev/null +++ b/cinder/zonemanager/drivers/brocade/brcd_fc_san_lookup_service.py @@ -0,0 +1,276 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +import paramiko + +from oslo.config import cfg + +from cinder import exception +from cinder.openstack.common import excutils +from cinder.openstack.common import log as logging +from cinder import utils +import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant +from cinder.zonemanager.drivers.fc_common import FCCommon + +LOG = logging.getLogger(__name__) + + +CONF = cfg.CONF +CONF.import_opt('fc_fabric_names', 'cinder.zonemanager.drivers.fc_common') + + +class BrcdFCSanLookupService(FCCommon): + + def __init__(self, **kwargs): + """Initializing the client.""" + super(BrcdFCSanLookupService, self).__init__(**kwargs) + self.configuration = kwargs.get('configuration', None) + self.create_configuration() + self.client = paramiko.SSHClient() + self.client.load_system_host_keys() + self.client.set_missing_host_key_policy(paramiko.WarningPolicy()) + + def create_configuration(self): + """Configuration specific to SAN context values.""" + config = self.configuration + + fc_fabric_opts = [] + fabric_names = config.fc_fabric_names.split(',') + LOG.debug(_('Fabric Names: %s'), fabric_names) + + # There can be more than one SAN in the network and we need to + # get credentials for each for SAN context lookup later. + if len(fabric_names) > 0: + for fabric_name in fabric_names: + fc_fabric_opts.append(cfg.StrOpt('fc_fabric_address_' + + fabric_name, + default='', + help='Management IP ' + 'of fabric')) + fc_fabric_opts.append(cfg.StrOpt('fc_fabric_user_' + + fabric_name, + default='', + help='Fabric user ID')) + fc_fabric_opts.append(cfg.StrOpt('fc_fabric_password_' + + fabric_name, + default='', + help='Password for user', + secret=True)) + fc_fabric_opts.append(cfg.IntOpt('fc_fabric_port_' + + fabric_name, default=22, + help='Connecting port')) + fc_fabric_opts.append(cfg.StrOpt('principal_switch_wwn_' + + fabric_name, + default=fabric_name, + help='Principal switch WWN ' + 'of the fabric')) + config.append_config_values(fc_fabric_opts) + + def get_device_mapping_from_network(self, + initiator_wwn_list, + target_wwn_list): + """Provides the initiator/target map for available SAN contexts. + + Looks up nameserver of each fc SAN configured to find logged in devices + and returns a map of initiator and target port WWNs for each fabric. + + :param initiator_wwn_list: List of initiator port WWN + :param target_wwn_list: List of target port WWN + :returns List -- device wwn map in following format + { + : { + 'initiator_port_wwn_list': + ('200000051e55a100', '200000051e55a121'..) + 'target_port_wwn_list': + ('100000051e55a100', '100000051e55a121'..) + } + } + :raises Exception when connection to fabric is failed + """ + device_map = {} + formatted_target_list = [] + formatted_initiator_list = [] + fabric_map = {} + fabric_names = self.configuration.fc_fabric_names + fabrics = None + if not fabric_names: + raise exception.InvalidParameterValue( + err=_("Missing Fibre Channel SAN configuration " + "param - fc_fabric_names")) + + fabrics = fabric_names.split(',') + LOG.debug(_("FC Fabric List: %s"), fabrics) + if fabrics: + for t in target_wwn_list: + formatted_target_list.append(self.get_formatted_wwn(t)) + + for i in initiator_wwn_list: + formatted_initiator_list.append(self. + get_formatted_wwn(i)) + + for fabric_name in fabrics: + fabric_ip = self.configuration.safe_get('fc_fabric_address_' + + fabric_name) + fabric_user = self.configuration.safe_get('fc_fabric_user_' + + fabric_name) + fabric_pwd = self.configuration.safe_get('fc_fabric_password_' + + fabric_name) + fabric_port = self.configuration.safe_get( + 'fc_fabric_port_' + fabric_name) + fabric_principal_wwn = self.configuration.safe_get( + 'principal_switch_wwn_' + + fabric_name) + + # Get name server data from fabric and find the targets + # logged in + nsinfo = '' + try: + LOG.debug(_("Getting name server data for " + "fabric %s"), fabric_ip) + self.client.connect( + fabric_ip, fabric_port, fabric_user, fabric_pwd) + nsinfo = self.get_nameserver_info() + except exception.FCSanLookupServiceException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed collecting name server info from " + "fabric %s") % fabric_ip) + except Exception as e: + msg = _("SSH connection failed " + "for %(fabric) with error: %(err)" + ) % {'fabric': fabric_ip, 'err': str(e)} + LOG.error(msg) + raise exception.FCSanLookupServiceException(message=msg) + finally: + self.close_connection() + LOG.debug(_("Lookup service:nsinfo-%s"), nsinfo) + LOG.debug(_("Lookup service:initiator list from " + "caller-%s"), formatted_initiator_list) + LOG.debug(_("Lookup service:target list from " + "caller-%s"), formatted_target_list) + visible_targets = filter(lambda x: x in formatted_target_list, + nsinfo) + visible_initiators = filter(lambda x: x in + formatted_initiator_list, nsinfo) + + if visible_targets: + LOG.debug(_("Filtered targets is: %s"), visible_targets) + # getting rid of the : before returning + for idx, elem in enumerate(visible_targets): + elem = str(elem).replace(':', '') + visible_targets[idx] = elem + else: + LOG.debug(_("No targets are in the nameserver for SAN %s"), + fabric_name) + + if visible_initiators: + # getting rid of the : before returning ~sk + for idx, elem in enumerate(visible_initiators): + elem = str(elem).replace(':', '') + visible_initiators[idx] = elem + else: + LOG.debug(_("No initiators are in the nameserver " + "for SAN %s"), fabric_name) + + fabric_map = { + 'initiator_port_wwn_list': visible_initiators, + 'target_port_wwn_list': visible_targets + } + device_map[fabric_principal_wwn] = fabric_map + LOG.debug(_("Device map for SAN context: %s"), device_map) + return device_map + + def get_nameserver_info(self): + """Get name server data from fabric. + + This method will return the connected node port wwn list(local + and remote) for the given switch fabric + """ + cli_output = None + nsinfo_list = [] + try: + cli_output = self._get_switch_data(ZoneConstant.NS_SHOW) + except exception.FCSanLookupServiceException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed collecting nsshow info for fabric")) + if cli_output: + nsinfo_list = self._parse_ns_output(cli_output) + try: + cli_output = self._get_switch_data(ZoneConstant.NS_CAM_SHOW) + except exception.FCSanLookupServiceException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed collecting nscamshow")) + if cli_output: + nsinfo_list.extend(self._parse_ns_output(cli_output)) + LOG.debug(_("Connector returning nsinfo-%s"), nsinfo_list) + return nsinfo_list + + def close_connection(self): + """This will close the client connection.""" + self.client.close() + self.client = None + + def _get_switch_data(self, cmd): + stdin, stdout, stderr = None, None, None + utils.check_ssh_injection([cmd]) + try: + stdin, stdout, stderr = self.client.exec_command(cmd) + switch_data = stdout.readlines() + except paramiko.SSHException as e: + msg = (_("SSH Command failed with error '%(err)r' " + "'%(command)s'") % {'err': str(e), 'command': cmd}) + LOG.error(msg) + raise exception.FCSanLookupServiceException(message=msg) + finally: + if (stdin): + stdin.flush() + stdin.close() + if (stdout): + stdout.close() + if (stderr): + stderr.close() + return switch_data + + def _parse_ns_output(self, switch_data): + """Parses name server data. + + Parses nameserver raw data and adds the device port wwns to the list + + :returns list of device port wwn from ns info + """ + nsinfo_list = [] + for line in switch_data: + if not(" NL " in line or " N " in line): + continue + linesplit = line.split(';') + if len(linesplit) > 2: + node_port_wwn = linesplit[2] + nsinfo_list.append(node_port_wwn) + else: + msg = _("Malformed nameserver string: %s") % line + LOG.error(msg) + raise exception.InvalidParameterValue(err=msg) + return nsinfo_list + + def get_formatted_wwn(self, wwn_str): + """Utility API that formats WWN to insert ':'.""" + if (len(wwn_str) != 16): + return wwn_str.lower() + else: + return (':'.join([wwn_str[i:i + 2] + for i in range(0, len(wwn_str), 2)])).lower() diff --git a/cinder/zonemanager/drivers/brocade/brcd_fc_zone_client_cli.py b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_client_cli.py new file mode 100644 index 000000000..78f0053df --- /dev/null +++ b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_client_cli.py @@ -0,0 +1,543 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +""" +Script to push the zone configuration to brocade SAN switches. +""" + +import random +import re + +from eventlet import greenthread + +from cinder import exception +from cinder.openstack.common import excutils +from cinder.openstack.common import log as logging +from cinder.openstack.common import processutils +from cinder import utils +import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant + +LOG = logging.getLogger(__name__) + + +class BrcdFCZoneClientCLI(object): + switch_ip = None + switch_port = '22' + switch_user = 'admin' + switch_pwd = 'none' + patrn = re.compile('[;\s]+') + + def __init__(self, ipaddress, username, password, port): + """initializing the client.""" + self.switch_ip = ipaddress + self.switch_port = port + self.switch_user = username + self.switch_pwd = password + self.sshpool = None + + def get_active_zone_set(self): + """Return the active zone configuration. + + Return active zoneset from fabric. When none of the configurations + are active then it will return empty map. + + :returns: Map -- active zone set map in the following format + { + 'zones': + {'openstack50060b0000c26604201900051ee8e329': + ['50060b0000c26604', '201900051ee8e329'] + }, + 'active_zone_config': 'OpenStack_Cfg' + } + """ + zone_set = {} + zone = {} + zone_member = None + zone_name = None + switch_data = None + zone_set_name = None + try: + switch_data = self._get_switch_info( + [ZoneConstant.GET_ACTIVE_ZONE_CFG]) + except exception.BrocadeZoningCliException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed getting active zone set " + "from fabric %s"), self.switch_ip) + try: + for line in switch_data: + line_split = re.split('\\t', line) + if len(line_split) > 2: + line_split = [x.replace( + '\n', '') for x in line_split] + line_split = [x.replace( + ' ', + '') for x in line_split] + if ZoneConstant.CFG_ZONESET in line_split: + zone_set_name = line_split[1] + continue + if line_split[1]: + zone_name = line_split[1] + zone[zone_name] = list() + if line_split[2]: + zone_member = line_split[2] + if zone_member: + zone_member_list = zone.get(zone_name) + zone_member_list.append(zone_member) + zone_set[ZoneConstant.CFG_ZONES] = zone + zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG] = zone_set_name + except Exception as ex: + # Incase of parsing error here, it should be malformed cli output. + msg = _("Malformed zone configuration: (switch=%(switch)s " + "zone_config=%(zone_config)s)." + ) % {'switch': self.switch_ip, + 'zone_config': switch_data} + LOG.error(msg) + LOG.exception(ex) + raise exception.FCZoneDriverException(reason=msg) + switch_data = None + return zone_set + + def add_zones(self, zones, activate): + """Add zone configuration. + + This method will add the zone configuration passed by user. + input params: + zones - zone names mapped to members. + zone members are colon separated but case-insensitive + { zonename1:[zonememeber1,zonemember2,...], + zonename2:[zonemember1, zonemember2,...]...} + e.g: {'openstack50060b0000c26604201900051ee8e329': + ['50:06:0b:00:00:c2:66:04', '20:19:00:05:1e:e8:e3:29'] + } + activate - True/False + """ + LOG.debug(_("Add Zones - Zones passed: %s"), zones) + cfg_name = None + iterator_count = 0 + zone_with_sep = '' + active_zone_set = self.get_active_zone_set() + LOG.debug(_("Active zone set:%s"), active_zone_set) + zone_list = active_zone_set[ZoneConstant.CFG_ZONES] + LOG.debug(_("zone list:%s"), zone_list) + for zone in zones.keys(): + # if zone exists, its an update. Delete & insert + # TODO(skolathur): This can be optimized to an update call later + LOG.debug("Update call") + if (zone in zone_list): + try: + self.delete_zones(zone, activate) + except exception.BrocadeZoningCliException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Deleting zone failed %s"), zone) + LOG.debug(_("Deleted Zone before insert : %s"), zone) + zone_members_with_sep = ';'.join(str(member) for + member in zones[zone]) + LOG.debug(_("Forming command for add zone")) + cmd = 'zonecreate "%(zone)s", "%(zone_members_with_sep)s"' % { + 'zone': zone, + 'zone_members_with_sep': zone_members_with_sep} + LOG.debug(_("Adding zone, cmd to run %s"), cmd) + self.apply_zone_change(cmd.split()) + LOG.debug(_("Created zones on the switch")) + if(iterator_count > 0): + zone_with_sep += ';' + iterator_count += 1 + zone_with_sep += zone + try: + cfg_name = active_zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG] + cmd = None + if not cfg_name: + cfg_name = ZoneConstant.OPENSTACK_CFG_NAME + cmd = 'cfgcreate "%(zoneset)s", "%(zones)s"' \ + % {'zoneset': cfg_name, 'zones': zone_with_sep} + else: + cmd = 'cfgadd "%(zoneset)s", "%(zones)s"' \ + % {'zoneset': cfg_name, 'zones': zone_with_sep} + LOG.debug(_("New zone %s"), cmd) + self.apply_zone_change(cmd.split()) + self._cfg_save() + if activate: + self.activate_zoneset(cfg_name) + except Exception as e: + self._cfg_trans_abort() + msg = _("Creating and activating zone set failed: " + "(Zone set=%(cfg_name)s error=%(err)s)." + ) % {'cfg_name': cfg_name, 'err': str(e)} + LOG.error(msg) + raise exception.BrocadeZoningCliException(reason=msg) + + def activate_zoneset(self, cfgname): + """Method to Activate the zone config. Param cfgname - ZonesetName.""" + cmd_list = [ZoneConstant.ACTIVATE_ZONESET, cfgname] + return self._ssh_execute(cmd_list, True, 1) + + def deactivate_zoneset(self): + """Method to deActivate the zone config.""" + return self._ssh_execute([ZoneConstant.DEACTIVATE_ZONESET], True, 1) + + def delete_zones(self, zone_names, activate): + """Delete zones from fabric. + + Method to delete the active zone config zones + + params zone_names: zoneNames separated by semicolon + params activate: True/False + """ + active_zoneset_name = None + active_zone_set = None + zone_list = [] + active_zone_set = self.get_active_zone_set() + active_zoneset_name = active_zone_set[ + ZoneConstant.ACTIVE_ZONE_CONFIG] + zone_list = active_zone_set[ZoneConstant.CFG_ZONES] + zones = self.patrn.split(''.join(zone_names)) + cmd = None + try: + if len(zones) == len(zone_list): + self.deactivate_zoneset() + cmd = 'cfgdelete "%(active_zoneset_name)s"' \ + % {'active_zoneset_name': active_zoneset_name} + # Active zoneset is being deleted, hence reset is_active + activate = False + else: + cmd = 'cfgremove "%(active_zoneset_name)s", "%(zone_names)s"' \ + % {'active_zoneset_name': active_zoneset_name, + 'zone_names': zone_names + } + LOG.debug(_("Delete zones: Config cmd to run:%s"), cmd) + self.apply_zone_change(cmd.split()) + for zone in zones: + self._zone_delete(zone) + self._cfg_save() + if activate: + self.activate_zoneset(active_zoneset_name) + except Exception as e: + msg = _("Deleting zones failed: (command=%(cmd)s error=%(err)s)." + ) % {'cmd': cmd, 'err': str(e)} + LOG.error(msg) + self._cfg_trans_abort() + raise exception.BrocadeZoningCliException(reason=msg) + + def get_nameserver_info(self): + """Get name server data from fabric. + + This method will return the connected node port wwn list(local + and remote) for the given switch fabric + """ + cli_output = None + return_list = [] + try: + cli_output = self._get_switch_info([ZoneConstant.NS_SHOW]) + except exception.BrocadeZoningCliException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed collecting nsshow " + "info for fabric %s"), self.switch_ip) + if (cli_output): + return_list = self._parse_ns_output(cli_output) + try: + cli_output = self._get_switch_info([ZoneConstant.NS_CAM_SHOW]) + except exception.BrocadeZoningCliException: + with excutils.save_and_reraise_exception(): + LOG.error(_("Failed collecting nscamshow " + "info for fabric %s"), self.switch_ip) + if (cli_output): + return_list.extend(self._parse_ns_output(cli_output)) + cli_output = None + return return_list + + def _cfg_save(self): + self._ssh_execute([ZoneConstant.CFG_SAVE], True, 1) + + def _zone_delete(self, zone_name): + cmd = 'zonedelete "%(zone_name)s"' % {'zone_name': zone_name} + self.apply_zone_change(cmd.split()) + + def _cfg_trans_abort(self): + is_abortable = self._is_trans_abortable() + if(is_abortable): + self.apply_zone_change([ZoneConstant.CFG_ZONE_TRANS_ABORT]) + + def _is_trans_abortable(self): + is_abortable = False + stdout, stderr = None, None + stdout, stderr = self._run_ssh( + [ZoneConstant.CFG_SHOW_TRANS], True, 1) + output = stdout.splitlines() + is_abortable = False + for line in output: + if(ZoneConstant.TRANS_ABORTABLE in line): + is_abortable = True + break + if stderr: + msg = _("Error while checking transaction status: %s") % stderr + raise exception.BrocadeZoningCliException(reason=msg) + else: + return is_abortable + + def apply_zone_change(self, cmd_list): + """Execute zoning cli with no status update. + + Executes CLI commands such as addZone where status return is + not expected. + """ + stdout, stderr = None, None + LOG.debug(_("Executing command via ssh: %s"), cmd_list) + stdout, stderr = self._run_ssh(cmd_list, True, 1) + # no output expected, so output means there is an error + if stdout: + msg = _("Error while running zoning CLI: (command=%(cmd)s " + "error=%(err)s).") % {'cmd': cmd_list, 'err': stdout} + LOG.error(msg) + self._cfg_trans_abort() + raise exception.BrocadeZoningCliException(reason=msg) + + def is_supported_firmware(self): + """Check firmware version is v6.4 or higher. + + This API checks if the firmware version per the plug-in support level. + This only checks major and minor version. + """ + cmd = ['version'] + firmware = 0 + try: + stdout, stderr = self._execute_shell_cmd(cmd) + if (stdout): + for line in stdout: + if 'Fabric OS: v' in line: + LOG.debug(_("Firmware version string:%s"), line) + ver = line.split('Fabric OS: v')[1].split('.') + if (ver): + firmware = int(ver[0] + ver[1]) + return firmware > 63 + else: + LOG.error(_("No CLI output for firmware version check")) + return False + except processutils.ProcessExecutionError as e: + msg = _("Error while getting data via ssh: (command=%(cmd)s " + "error=%(err)s).") % {'cmd': cmd, 'err': str(e)} + LOG.error(msg) + raise exception.BrocadeZoningCliException(reason=msg) + + def _get_switch_info(self, cmd_list): + stdout, stderr, sw_data = None, None, None + try: + stdout, stderr = self._run_ssh(cmd_list, True, 1) + if (stdout): + sw_data = stdout.splitlines() + return sw_data + except processutils.ProcessExecutionError as e: + msg = _("Error while getting data via ssh: (command=%(cmd)s " + "error=%(err)s).") % {'cmd': cmd_list, 'err': str(e)} + LOG.error(msg) + raise exception.BrocadeZoningCliException(reason=msg) + + def _parse_ns_output(self, switch_data): + """Parses name server data. + + Parses nameserver raw data and adds the device port wwns to the list + + :returns: List -- list of device port wwn from ns info + """ + return_list = [] + for line in switch_data: + if not(" NL " in line or " N " in line): + continue + linesplit = line.split(';') + if len(linesplit) > 2: + node_port_wwn = linesplit[2] + return_list.append(node_port_wwn) + else: + msg = _("Malformed nameserver string: %s") % line + LOG.error(msg) + raise exception.InvalidParameterValue(err=msg) + return return_list + + def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1): + # TODO(skolathur): Need to implement ssh_injection check + # currently, the check will fail for zonecreate command + # as zone members are separated by ';'which is a danger char + command = ' '. join(cmd_list) + + if not self.sshpool: + self.sshpool = utils.SSHPool(self.switch_ip, + self.switch_port, + None, + self.switch_user, + self.switch_pwd, + min_size=1, + max_size=5) + last_exception = None + try: + with self.sshpool.item() as ssh: + while attempts > 0: + attempts -= 1 + try: + return processutils.ssh_execute( + ssh, + command, + check_exit_code=check_exit_code) + except Exception as e: + LOG.error(e) + last_exception = e + greenthread.sleep(random.randint(20, 500) / 100.0) + try: + raise processutils.ProcessExecutionError( + exit_code=last_exception.exit_code, + stdout=last_exception.stdout, + stderr=last_exception.stderr, + cmd=last_exception.cmd) + except AttributeError: + raise processutils.ProcessExecutionError( + exit_code=-1, + stdout="", + stderr="Error running SSH command", + cmd=command) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_("Error running SSH command: %s") % command) + + def _ssh_execute(self, cmd_list, check_exit_code=True, attempts=1): + """Execute cli with status update. + + Executes CLI commands such as cfgsave where status return is expected. + """ + utils.check_ssh_injection(cmd_list) + command = ' '. join(cmd_list) + + if not self.sshpool: + self.sshpool = utils.SSHPool(self.switch_ip, + self.switch_port, + None, + self.switch_user, + self.switch_pwd, + min_size=1, + max_size=5) + stdin, stdout, stderr = None, None, None + LOG.debug(_("Executing command via ssh: %s") % command) + last_exception = None + try: + with self.sshpool.item() as ssh: + while attempts > 0: + attempts -= 1 + try: + stdin, stdout, stderr = ssh.exec_command(command) + greenthread.sleep(random.randint(20, 500) / 100.0) + stdin.write("%s\n" % ZoneConstant.YES) + channel = stdout.channel + exit_status = channel.recv_exit_status() + LOG.debug(_("Exit Status from ssh:%s"), exit_status) + # exit_status == -1 if no exit code was returned + if exit_status != -1: + LOG.debug(_('Result was %s') % exit_status) + if check_exit_code and exit_status != 0: + raise processutils.ProcessExecutionError( + exit_code=exit_status, + stdout=stdout, + stderr=stderr, + cmd=command) + else: + return True + else: + return True + except Exception as e: + LOG.error(e) + last_exception = e + greenthread.sleep(random.randint(20, 500) / 100.0) + LOG.debug(_("Handling error case after " + "SSH:%s"), str(last_exception)) + try: + raise processutils.ProcessExecutionError( + exit_code=last_exception.exit_code, + stdout=last_exception.stdout, + stderr=last_exception.stderr, + cmd=last_exception.cmd) + except AttributeError: + raise processutils.ProcessExecutionError( + exit_code=-1, + stdout="", + stderr="Error running SSH command", + cmd=command) + except Exception as e: + with excutils.save_and_reraise_exception(): + LOG.error(_("Error executing command via ssh: %s"), str(e)) + finally: + if stdin: + stdin.flush() + stdin.close() + if stdout: + stdout.close() + if stderr: + stderr.close() + + def _execute_shell_cmd(self, cmd): + """Run command over shell for older firmware versions. + + We invoke shell and issue the command and return the output. + This is primarily used for issuing read commands when we are not sure + if the firmware supports exec_command. + """ + utils.check_ssh_injection(cmd) + command = ' '. join(cmd) + stdout, stderr = None, None + if not self.sshpool: + self.sshpool = utils.SSHPool(self.switch_ip, + self.switch_port, + None, + self.switch_user, + self.switch_pwd, + min_size=1, + max_size=5) + with self.sshpool.item() as ssh: + LOG.debug('Running cmd (SSH): %s' % command) + channel = ssh.invoke_shell() + stdin_stream = channel.makefile('wb') + stdout_stream = channel.makefile('rb') + stderr_stream = channel.makefile('rb') + stdin_stream.write('''%s +exit +''' % command) + stdin_stream.flush() + stdout = stdout_stream.readlines() + stderr = stderr_stream.readlines() + stdin_stream.close() + stdout_stream.close() + stderr_stream.close() + + exit_status = channel.recv_exit_status() + # exit_status == -1 if no exit code was returned + if exit_status != -1: + LOG.debug('Result was %s' % exit_status) + if exit_status != 0: + msg = "command %s failed" % command + LOG.debug(msg) + raise processutils.ProcessExecutionError( + exit_code=exit_status, + stdout=stdout, + stderr=stderr, + cmd=command) + try: + channel.close() + except Exception as e: + LOG.exception(e) + LOG.debug("_execute_cmd: stdout to return:%s" % stdout) + LOG.debug("_execute_cmd: stderr to return:%s" % stderr) + return (stdout, stderr) + + def cleanup(self): + self.sshpool = None diff --git a/cinder/zonemanager/drivers/brocade/brcd_fc_zone_driver.py b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_driver.py new file mode 100644 index 000000000..f84020d42 --- /dev/null +++ b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_driver.py @@ -0,0 +1,529 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +""" +Brocade Zone Driver is responsible to manage access control using FC zoning +for Brocade FC fabrics. +This is a concrete implementation of FCZoneDriver interface implementing +add_connection and delete_connection interfaces. + +**Related Flags** + +:zone_activate: Used by: class: 'FCZoneDriver'. Defaults to True +:zone_name_prefix: Used by: class: 'FCZoneDriver'. Defaults to 'openstack' +""" + + +from oslo.config import cfg + +from cinder import exception +from cinder.openstack.common import excutils +from cinder.openstack.common import importutils +from cinder.openstack.common import lockutils +from cinder.openstack.common import log as logging +from cinder.zonemanager.drivers.fc_zone_driver import FCZoneDriver + +LOG = logging.getLogger(__name__) + +brcd_opts = [ + cfg.StrOpt('brcd_sb_connector', + default='cinder.zonemanager.drivers.brocade' + '.brcd_fc_zone_client_cli.BrcdFCZoneClientCLI', + help='Southbound connector for zoning operation'), +] + +CONF = cfg.CONF +CONF.register_opts(brcd_opts) +CONF.import_opt('zone_activate', 'cinder.zonemanager.drivers.fc_zone_driver') +CONF.import_opt('zone_name_prefix', + 'cinder.zonemanager.drivers.fc_zone_driver') +CONF.import_opt('fc_fabric_names', 'cinder.zonemanager.drivers.fc_common') + + +class BrcdFCZoneDriver(FCZoneDriver): + """Brocade FC zone driver implementation. + + OpenStack Fibre Channel zone driver to manage FC zoning in + Brocade SAN fabrics. + + Version history: + 1.0 - Initial Brocade FC zone driver + """ + + def __init__(self, **kwargs): + super(BrcdFCZoneDriver, self).__init__(**kwargs) + self.configuration = kwargs.get('configuration', None) + if self.configuration: + self.configuration.append_config_values(brcd_opts) + # Adding a hack to hendle parameters from super classes + # in case configured with multi backend. + fabric_names = self.configuration.safe_get('fc_fabric_names') + activate = self.configuration.safe_get('zone_activate') + prefix = self.configuration.safe_get('zone_name_prefix') + base_san_opts = [] + if not fabric_names: + base_san_opts.append( + cfg.StrOpt('fc_fabric_names', default=None, + help='Comma separated list of fibre channel ' + 'fabric names. This list of names is used to' + ' retrieve other SAN credentials for connecting' + ' to each SAN fabric' + )) + if not activate: + base_san_opts.append( + cfg.BoolOpt('zone_activate', + default=True, + help='Indicates whether zone should ' + 'be activated or not')) + if not prefix: + base_san_opts.append( + cfg.StrOpt('zone_name_prefix', + default="openstack", + help="A prefix to be used when naming zone")) + if len(base_san_opts) > 0: + CONF.register_opts(base_san_opts) + self.configuration.append_config_values(base_san_opts) + fabric_names = self.configuration.fc_fabric_names.split(',') + fc_fabric_opts = [] + # There can be more than one SAN in the network and we need to + # get credentials for each SAN. + if fabric_names: + for fabric_name in fabric_names: + fc_fabric_opts.append(cfg.StrOpt('fc_fabric_address_' + + fabric_name, + default='', + help='Management IP' + ' of fabric')) + fc_fabric_opts.append(cfg.StrOpt('fc_fabric_user_' + + fabric_name, + default='', + help='Fabric user ID')) + fc_fabric_opts.append(cfg.StrOpt('fc_fabric_password_' + + fabric_name, + default='', + help='Password for user', + secret=True)) + fc_fabric_opts.append(cfg.IntOpt('fc_fabric_port_' + + fabric_name, + default=22, + help='Connecting port')) + fc_fabric_opts.append(cfg.StrOpt('zoning_policy_' + + fabric_name, + default=self.configuration + .zoning_policy, + help='overridden ' + 'zoning policy')) + fc_fabric_opts.append(cfg.BoolOpt('zone_activate_' + + fabric_name, + default=self + .configuration + .zone_activate, + help='overridden zoning ' + 'activation state')) + fc_fabric_opts.append(cfg.StrOpt('zone_name_prefix_' + + fabric_name, + default=self.configuration + .zone_name_prefix, + help='overridden zone ' + 'name prefix')) + fc_fabric_opts.append(cfg.StrOpt('principal_switch_wwn_' + + fabric_name, + default=fabric_name, + help='Principal switch ' + 'WWN of the fabric')) + self.configuration.append_config_values(fc_fabric_opts) + + def get_formatted_wwn(self, wwn_str): + """Utility API that formats WWN to insert ':'.""" + wwn_str = wwn_str.encode('ascii') + if len(wwn_str) != 16: + return wwn_str + else: + return ':'.join( + [wwn_str[i:i + 2] for i in range(0, len(wwn_str), 2)]) + + @lockutils.synchronized('brcd', 'fcfabric-', True) + def add_connection(self, fabric, initiator_target_map): + """Concrete implementation of add_connection. + + Based on zoning policy and state of each I-T pair, list of zone + members are created and pushed to the fabric to add zones. The + new zones created or zones updated are activated based on isActivate + flag set in cinder.conf returned by volume driver after attach + operation. + + :param fabric: Fabric name from cinder.conf file + :param initiator_target_map: Mapping of initiator to list of targets + """ + LOG.debug(_("Add connection for Fabric:%s"), fabric) + LOG.info(_("BrcdFCZoneDriver - Add connection " + "for I-T map: %s"), initiator_target_map) + fabric_ip = self.configuration.safe_get( + 'fc_fabric_address_' + fabric) + fabric_user = self.configuration.safe_get( + 'fc_fabric_user_' + fabric) + fabric_pwd = self.configuration.safe_get( + 'fc_fabric_password_' + fabric) + fabric_port = self.configuration.safe_get( + 'fc_fabric_port_' + fabric) + zoning_policy = self.configuration.zoning_policy + zoning_policy_fab = self.configuration.safe_get( + 'zoning_policy_' + fabric) + if zoning_policy_fab: + zoning_policy = zoning_policy_fab + + LOG.info(_("Zoning policy for Fabric %s"), zoning_policy) + cli_client = None + try: + cli_client = importutils.import_object( + self.configuration.brcd_sb_connector, + ipaddress=fabric_ip, + username=fabric_user, + password=fabric_pwd, + port=fabric_port) + if not cli_client.is_supported_firmware(): + msg = _("Unsupported firmware on switch %s. Make sure " + "switch is running firmware v6.4 or higher" + ) % fabric_ip + LOG.error(msg) + raise exception.FCZoneDriverException(msg) + except exception.BrocadeZoningCliException as brocade_ex: + raise exception.FCZoneDriverException(str(brocade_ex)) + except Exception as e: + LOG.error(str(e)) + msg = _("Failed to add zoning configuration %s" + ) % str(e) + raise exception.FCZoneDriverException(msg) + + cfgmap_from_fabric = self.get_active_zone_set( + fabric_ip, fabric_user, fabric_pwd, fabric_port) + zone_names = [] + if cfgmap_from_fabric.get('zones'): + zone_names = cfgmap_from_fabric['zones'].keys() + # based on zoning policy, create zone member list and + # push changes to fabric. + for initiator_key in initiator_target_map.keys(): + zone_map = {} + initiator = initiator_key.lower() + t_list = initiator_target_map[initiator_key] + if zoning_policy == 'initiator-target': + for t in t_list: + target = t.lower() + zone_members = [self.get_formatted_wwn(initiator), + self.get_formatted_wwn(target)] + zone_name = (self.configuration.zone_name_prefix + + initiator.replace(':', '') + + target.replace(':', '')) + if ( + len(cfgmap_from_fabric) == 0 or ( + zone_name not in zone_names)): + zone_map[zone_name] = zone_members + else: + # This is I-T zoning, skip if zone already exists. + LOG.info(_("Zone exists in I-T mode. " + "Skipping zone creation %s"), zone_name) + elif zoning_policy == 'initiator': + zone_members = [self.get_formatted_wwn(initiator)] + for t in t_list: + target = t.lower() + zone_members.append(self.get_formatted_wwn(target)) + + zone_name = self.configuration.zone_name_prefix \ + + initiator.replace(':', '') + + if len(zone_names) > 0 and (zone_name in zone_names): + zone_members = zone_members + filter( + lambda x: x not in zone_members, + cfgmap_from_fabric['zones'][zone_name]) + + zone_map[zone_name] = zone_members + else: + msg = _("Zoning Policy: %s, not " + "recognized") % zoning_policy + LOG.error(msg) + raise exception.FCZoneDriverException(msg) + + LOG.info(_("Zone map to add: %s"), zone_map) + + if len(zone_map) > 0: + try: + cli_client.add_zones( + zone_map, self.configuration.zone_activate) + cli_client.cleanup() + except exception.BrocadeZoningCliException as brocade_ex: + raise exception.FCZoneDriverException(str(brocade_ex)) + except Exception as e: + LOG.error(str(e)) + msg = _("Failed to add zoning configuration %s" + ) % str(e) + raise exception.FCZoneDriverException(msg) + LOG.debug(_("Zones added successfully: %s"), zone_map) + + @lockutils.synchronized('brcd', 'fcfabric-', True) + def delete_connection(self, fabric, initiator_target_map): + """Concrete implementation of delete_connection. + + Based on zoning policy and state of each I-T pair, list of zones + are created for deletion. The zones are either updated deleted based + on the policy and attach/detach state of each I-T pair. + + :param fabric: Fabric name from cinder.conf file + :param initiator_target_map: Mapping of initiator to list of targets + """ + LOG.debug(_("Delete connection for fabric:%s"), fabric) + LOG.info(_("BrcdFCZoneDriver - Delete connection for I-T map: %s"), + initiator_target_map) + fabric_ip = self.configuration.safe_get( + 'fc_fabric_address_' + fabric) + fabric_user = self.configuration.safe_get( + 'fc_fabric_user_' + fabric) + fabric_pwd = self.configuration.safe_get( + 'fc_fabric_password_' + fabric) + fabric_port = self.configuration.safe_get( + 'fc_fabric_port_' + fabric) + zoning_policy = self.configuration.zoning_policy + zoning_policy_fab = self.configuration.safe_get( + 'zoning_policy_' + fabric) + if zoning_policy_fab: + zoning_policy = zoning_policy_fab + + LOG.info(_("Zoning policy for fabric %s"), zoning_policy) + conn = None + try: + conn = importutils.import_object( + self.configuration.brcd_sb_connector, + ipaddress=fabric_ip, + username=fabric_user, + password=fabric_pwd, + port=fabric_port) + if not conn.is_supported_firmware(): + msg = _("Unsupported firmware on switch %s. Make sure " + "switch is running firmware v6.4 or higher" + ) % fabric_ip + LOG.error(msg) + raise exception.FCZoneDriverException(msg) + except exception.BrocadeZoningCliException as brocade_ex: + raise exception.FCZoneDriverException(str(brocade_ex)) + except Exception as e: + LOG.error(str(e)) + msg = _("Failed to delete zoning configuration %s" + ) % str(e) + raise exception.FCZoneDriverException(msg) + + cfgmap_from_fabric = self.get_active_zone_set( + fabric_ip, fabric_user, fabric_pwd, fabric_port) + zone_names = [] + if cfgmap_from_fabric.get('zones'): + zone_names = cfgmap_from_fabric['zones'].keys() + + # Based on zoning policy, get zone member list and push changes to + # fabric. This operation could result in an update for zone config + # with new member list or deleting zones from active cfg. + LOG.debug(_("zone config from Fabric: %s"), cfgmap_from_fabric) + for initiator_key in initiator_target_map.keys(): + initiator = initiator_key.lower() + formatted_initiator = self.get_formatted_wwn(initiator) + zone_map = {} + zones_to_delete = [] + t_list = initiator_target_map[initiator_key] + if zoning_policy == 'initiator-target': + # In this case, zone needs to be deleted. + for t in t_list: + target = t.lower() + zone_name = ( + self.configuration.zone_name_prefix + + initiator.replace(':', '') + + target.replace(':', '')) + LOG.debug(_("Zone name to del: %s"), zone_name) + if len(zone_names) > 0 and (zone_name in zone_names): + # delete zone. + LOG.debug(("Added zone to delete to " + "list: %s"), zone_name) + zones_to_delete.append(zone_name) + + elif zoning_policy == 'initiator': + zone_members = [formatted_initiator] + for t in t_list: + target = t.lower() + zone_members.append(self.get_formatted_wwn(target)) + + zone_name = self.configuration.zone_name_prefix \ + + initiator.replace(':', '') + + if (zone_names and (zone_name in zone_names)): + filtered_members = filter( + lambda x: x not in zone_members, + cfgmap_from_fabric['zones'][zone_name]) + + # The assumption here is that initiator is always there + # in the zone as it is 'initiator' policy. We find the + # filtered list and if it is non-empty, add initiator + # to it and update zone if filtered list is empty, we + # remove that zone. + LOG.debug(_("Zone delete - I mode: " + "filtered targets:%s"), filtered_members) + if filtered_members: + filtered_members.append(formatted_initiator) + LOG.debug(_("Filtered zone members to " + "update: %s"), filtered_members) + zone_map[zone_name] = filtered_members + LOG.debug(_("Filtered zone Map to " + "update: %s"), zone_map) + else: + zones_to_delete.append(zone_name) + else: + LOG.info(_("Zoning Policy: %s, not " + "recognized"), zoning_policy) + LOG.debug(_("Final Zone map to update: %s"), zone_map) + LOG.debug(_("Final Zone list to delete: %s"), zones_to_delete) + try: + # Update zone membership. + if zone_map: + conn.add_zones( + zone_map, self.configuration.zone_activate) + # Delete zones ~sk. + if zones_to_delete: + zone_name_string = '' + num_zones = len(zones_to_delete) + for i in range(0, num_zones): + if i == 0: + zone_name_string = ( + '%s%s' % ( + zone_name_string, zones_to_delete[i])) + else: + zone_name_string = '%s%s%s' % ( + zone_name_string, ';', zones_to_delete[i]) + + conn.delete_zones( + zone_name_string, self.configuration.zone_activate) + conn.cleanup() + except Exception as e: + LOG.error(str(e)) + msg = _("Failed to update or delete zoning configuration") + raise exception.FCZoneDriverException(msg) + + def get_san_context(self, target_wwn_list): + """Lookup SAN context for visible end devices. + + Look up each SAN configured and return a map of SAN (fabric IP) to + list of target WWNs visible to the fabric. + """ + # TODO(Santhosh Kolathur): consider refactoring to use lookup service. + formatted_target_list = [] + fabric_map = {} + fabrics = self.configuration.fc_fabric_names.split(',') + LOG.debug(_("Fabric List: %s"), fabrics) + LOG.debug(_("Target wwn List: %s"), target_wwn_list) + if len(fabrics) > 0: + for t in target_wwn_list: + formatted_target_list.append(self.get_formatted_wwn(t.lower())) + LOG.debug(_("Formatted Target wwn List:" + " %s"), formatted_target_list) + for fabric_name in fabrics: + fabric_ip = self.configuration.safe_get( + 'fc_fabric_address_' + fabric_name) + fabric_user = self.configuration.safe_get( + 'fc_fabric_user_' + fabric_name) + fabric_pwd = self.configuration.safe_get( + 'fc_fabric_password_' + fabric_name) + fabric_port = self.configuration.safe_get( + 'fc_fabric_port_' + fabric_name) + conn = None + try: + conn = importutils.import_object( + self.configuration.brcd_sb_connector, + ipaddress=fabric_ip, + username=fabric_user, + password=fabric_pwd, + port=fabric_port) + if not conn.is_supported_firmware(): + msg = _("Unsupported firmware on switch %s. Make sure " + "switch is running firmware v6.4 or higher" + ) % fabric_ip + LOG.error(msg) + raise exception.FCZoneDriverException(msg) + except exception.BrocadeZoningCliException as brocade_ex: + raise exception.FCZoneDriverException(str(brocade_ex)) + except Exception as e: + LOG.error(str(e)) + msg = _("Failed to get SAN context %s" + ) % str(e) + raise exception.FCZoneDriverException(msg) + + # Get name server data from fabric and get the targets + # logged in. + nsinfo = None + try: + nsinfo = conn.get_nameserver_info() + LOG.debug(_("name server info from fabric:%s"), nsinfo) + conn.cleanup() + except exception.BrocadeZoningCliException as ex: + with excutils.save_and_reraise_exception(): + LOG.error(_("Error getting name server " + "info: %s"), str(ex)) + except Exception as e: + msg = _("Failed to get name server info:%s") % str(e) + LOG.error(msg) + raise exception.FCZoneDriverException(msg) + visible_targets = filter( + lambda x: x in formatted_target_list, + nsinfo) + + if visible_targets: + LOG.info(_("Filtered targets for SAN is: %s"), + {fabric_name: visible_targets}) + # getting rid of the ':' before returning + for idx, elem in enumerate(visible_targets): + visible_targets[idx] = str( + visible_targets[idx]).replace(':', '') + fabric_map[fabric_name] = visible_targets + else: + LOG.debug(_("No targets are in the nameserver for SAN %s"), + fabric_name) + LOG.debug(_("Return SAN context output:%s"), fabric_map) + return fabric_map + + def get_active_zone_set(self, fabric_ip, + fabric_user, fabric_pwd, fabric_port): + """Gets active zone config from fabric.""" + cfgmap = {} + conn = None + try: + LOG.debug(_("Southbound connector:" + " %s"), self.configuration.brcd_sb_connector) + conn = importutils.import_object( + self.configuration.brcd_sb_connector, + ipaddress=fabric_ip, username=fabric_user, + password=fabric_pwd, port=fabric_port) + if not conn.is_supported_firmware(): + msg = _("Unsupported firmware on switch %s. Make sure " + "switch is running firmware v6.4 or higher" + ) % fabric_ip + LOG.error(msg) + raise exception.FCZoneDriverException(msg) + cfgmap = conn.get_active_zone_set() + conn.cleanup() + except exception.BrocadeZoningCliException as brocade_ex: + raise exception.FCZoneDriverException(str(brocade_ex)) + except Exception as e: + msg = _("Failed to access active zoning configuration:%s") % str(e) + LOG.error(msg) + raise exception.FCZoneDriverException(msg) + LOG.debug(_("Active zone set from fabric: %s"), cfgmap) + return cfgmap diff --git a/cinder/zonemanager/drivers/brocade/fc_zone_constants.py b/cinder/zonemanager/drivers/brocade/fc_zone_constants.py new file mode 100644 index 000000000..3ef01cde1 --- /dev/null +++ b/cinder/zonemanager/drivers/brocade/fc_zone_constants.py @@ -0,0 +1,47 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +""" +Common constants used by Brocade FC Zone Driver. +""" +YES = 'y' +ACTIVE_ZONE_CONFIG = 'active_zone_config' +CFG_ZONESET = 'cfg:' +CFG_ZONES = 'zones' +OPENSTACK_CFG_NAME = 'OpenStack_Cfg' +SUCCESS = 'Success' +TRANS_ABORTABLE = 'It is abortable' + +""" +CLI Commands for FC zoning operations. +""" +GET_ACTIVE_ZONE_CFG = 'cfgactvshow' +ZONE_CREATE = 'zonecreate ' +ZONESET_CREATE = 'cfgcreate ' +CFG_SAVE = 'cfgsave' +CFG_ADD = 'cfgadd ' +ACTIVATE_ZONESET = 'cfgenable ' +DEACTIVATE_ZONESET = 'cfgdisable' +CFG_DELETE = 'cfgdelete ' +CFG_REMOVE = 'cfgremove ' +ZONE_DELETE = 'zonedelete ' +CFG_SHOW_TRANS = 'cfgtransshow' +CFG_ZONE_TRANS_ABORT = 'cfgtransabort' +NS_SHOW = 'nsshow' +NS_CAM_SHOW = 'nscamshow' diff --git a/cinder/zonemanager/drivers/fc_common.py b/cinder/zonemanager/drivers/fc_common.py new file mode 100644 index 000000000..ce08c20c5 --- /dev/null +++ b/cinder/zonemanager/drivers/fc_common.py @@ -0,0 +1,38 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +from oslo.config import cfg + +san_context_opts = [ + cfg.StrOpt('fc_fabric_names', + default=None, + help='Comma separated list of fibre channel fabric names.' + ' This list of names is used to retrieve other SAN credentials' + ' for connecting to each SAN fabric'), +] + +CONF = cfg.CONF +CONF.register_opts(san_context_opts) + + +class FCCommon(object): + """Common interface for FC operations.""" + + def __init__(self, **kwargs): + pass diff --git a/cinder/zonemanager/drivers/fc_zone_driver.py b/cinder/zonemanager/drivers/fc_zone_driver.py new file mode 100644 index 000000000..3f5e45d7d --- /dev/null +++ b/cinder/zonemanager/drivers/fc_zone_driver.py @@ -0,0 +1,106 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +""" +Base Zone Driver is responsible to manage access control using FC zoning +Vendor specific implementations should extend this class to provide +concrete implementation for add_connection and delete_connection +interfaces. + +**Related Flags** + +:zoning_policy: Used by: class: 'FCZoneDriver'. Defaults to 'none' +:zone_driver: Used by: class: 'FCZoneDriver'. Defaults to 'none' + +""" + + +from oslo.config import cfg + +from cinder.openstack.common import log as logging +from cinder.zonemanager.drivers.fc_common import FCCommon + +LOG = logging.getLogger(__name__) + +fc_zone_opts = [ + cfg.BoolOpt('zone_activate', + default=True, + help="Indicates whether zone should be activated or not"), + cfg.StrOpt('zone_name_prefix', + default="openstack", + help="A prefix to be used when naming zone"), +] + +CONF = cfg.CONF +CONF.register_opts(fc_zone_opts) + + +class FCZoneDriver(FCCommon): + """Interface to manage Connection control during attach/detach.""" + + def __init__(self, **kwargs): + super(FCZoneDriver, self).__init__(**kwargs) + LOG.debug(_("Initializing FCZoneDriver")) + + def add_connection(self, fabric, initiator_target_map): + """Add connection control. + + Abstract method to add connection control. + All implementing drivers should provide concrete implementation + for this API. + :param fabric: Fabric name from cinder.conf file + :param initiator_target_map: Mapping of initiator to list of targets + Example initiator_target_map: + { + '10008c7cff523b01': ['20240002ac000a50', '20240002ac000a40'] + } + Note that WWPN can be in lower or upper case and can be + ':' separated strings + """ + raise NotImplementedError() + + def delete_connection(self, fabric, initiator_target_map): + """Delete connection control. + + Abstract method to remove connection control. + All implementing drivers should provide concrete implementation + for this API. + :param fabric: Fabric name from cinder.conf file + :param initiator_target_map: Mapping of initiator to list of targets + Example initiator_target_map: + { + '10008c7cff523b01': ['20240002ac000a50', '20240002ac000a40'] + } + Note that WWPN can be in lower or upper case and can be + ':' separated strings + """ + raise NotImplementedError() + + def get_san_context(self, target_wwn_list): + """Get SAN context for end devices. + + Abstract method to get SAN contexts for given list of end devices + All implementing drivers should provide concrete implementation + for this API. + :param fabric: Fabric name from cinder.conf file + :param initiator_target_map: Mapping of initiator to list of targets + Example initiator_target_map: ['20240002ac000a50', '20240002ac000a40'] + Note that WWPN can be in lower or upper case and can be + ':' separated strings + """ + raise NotImplementedError() diff --git a/cinder/zonemanager/fc_san_lookup_service.py b/cinder/zonemanager/fc_san_lookup_service.py new file mode 100644 index 000000000..27528f7dd --- /dev/null +++ b/cinder/zonemanager/fc_san_lookup_service.py @@ -0,0 +1,97 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +""" +Base Lookup Service for name server lookup to find the initiator to target port +mapping for available SAN contexts. +Vendor specific lookup classes are expected to implement the interfaces +defined in this class. +""" + + +from oslo.config import cfg + +from cinder import exception +from cinder.openstack.common import importutils +from cinder.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) +lookup_service_opts = [ + cfg.StrOpt('fc_san_lookup_service', + default='cinder.zonemanager.drivers.brocade' + '.brcd_fc_san_lookup_service.BrcdFCSanLookupService', + help='FC San Lookup Service'), +] + +CONF = cfg.CONF +CONF.register_opts(lookup_service_opts) + + +class FCSanLookupService(object): + """Base Lookup Service. + + Base Lookup Service for name server lookup to find the initiator to + target port mapping for available SAN contexts. + """ + + lookup_service = None + + def __init__(self, **kwargs): + self.configuration = kwargs.get('configuration', None) + if self.configuration: + self.configuration.append_config_values(lookup_service_opts) + + def get_device_mapping_from_network(self, initiator_list, target_list): + """Get device mapping from FC network. + + Gets a filtered list of initiator ports and target ports for each SAN + available. + :param initiator_list list of initiator port WWN + :param target_list list of target port WWN + :return device wwn map in following format + { + : { + 'initiator_port_wwn_list': + ('200000051E55A100', '200000051E55A121'..) + 'target_port_wwn_list': + ('100000051E55A100', '100000051E55A121'..) + } + } + :raise Exception when a lookup service implementation is not specified + in cinder.conf:fc_san_lookup_service + """ + # Initialize vendor specific implementation of FCZoneDriver + if (self.configuration.fc_san_lookup_service): + lookup_service = self.configuration.fc_san_lookup_service + LOG.debug(_("Lookup service to invoke: " + "%s"), lookup_service) + self.lookup_service = importutils.import_object( + lookup_service, configuration=self.configuration) + else: + msg = _("Lookup service not configured. Config option for " + "fc_san_lookup_service need to specify a concrete " + "implementation of lookup service") + LOG.error(msg) + raise exception.FCSanLookupServiceException(msg) + try: + device_map = self.lookup_service.get_device_mapping_from_network( + initiator_list, target_list) + except Exception as e: + LOG.error(str(e)) + raise exception.FCSanLookupServiceException(str(e)) + return device_map diff --git a/cinder/zonemanager/fc_zone_manager.py b/cinder/zonemanager/fc_zone_manager.py new file mode 100644 index 000000000..2fc4f8f1c --- /dev/null +++ b/cinder/zonemanager/fc_zone_manager.py @@ -0,0 +1,205 @@ +# (c) Copyright 2014 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +""" +ZoneManager is responsible to manage access control using FC zoning +when zoning mode is set as 'fabric'. +ZoneManager provides interfaces to add connection and remove connection +for given initiator and target list associated with a FC volume attach and +detach operation. + +**Related Flags** + +:zone_driver: Used by:class:`ZoneManager`. + Defaults to + `cinder.zonemanager.drivers.brocade.brcd_fc_zone_driver.BrcdFCZoneDriver` +:zoning_policy: Used by: class: 'ZoneManager'. Defaults to 'none' + +""" + + +from oslo.config import cfg + +from cinder import exception +from cinder.openstack.common import importutils +from cinder.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + +zone_manager_opts = [ + cfg.StrOpt('zone_driver', + default='cinder.zonemanager.drivers.brocade.brcd_fc_zone_driver' + '.BrcdFCZoneDriver', + help='FC Zone Driver responsible for zone management'), + cfg.StrOpt('zoning_policy', + default='initiator-target', + help='Zoning policy configured by user'), +] + +CONF = cfg.CONF +CONF.register_opts(zone_manager_opts) + + +class ZoneManager: + """Manages Connection control during attach/detach.""" + driver = None + fabric_names = [] + + def __init__(self, **kwargs): + """Load the driver from the one specified in args, or from flags.""" + + self.configuration = kwargs.get('configuration', None) + if self.configuration: + self.configuration.append_config_values(zone_manager_opts) + + zone_driver = self.configuration.zone_driver + LOG.debug(_("Zone Driver from config: {%s}"), zone_driver) + + # Initialize vendor specific implementation of FCZoneDriver + self.driver = importutils.import_object( + zone_driver, + configuration=self.configuration) + + def get_zoning_state_ref_count(self, initiator_wwn, target_wwn): + """Zone management state check. + + Performs state check for given I-T pair to return the current count of + active attach for the pair. + """ + # TODO(sk): ref count state management + count = 0 + # check the state for I-T pair + return count + + def add_connection(self, initiator_target_map): + """Add connection control. + + Adds connection control for the given initiator target map. + initiator_target_map - each initiator WWN mapped to a list of one + or more target WWN: + eg: + { + '10008c7cff523b01': ['20240002ac000a50', '20240002ac000a40'] + } + """ + connected_fabric = None + try: + for initiator in initiator_target_map.keys(): + target_list = initiator_target_map[initiator] + LOG.debug(_("Target List :%s"), {initiator: target_list}) + + # get SAN context for the target list + fabric_map = self.driver.get_san_context(target_list) + LOG.debug(_("Fabric Map after context lookup:%s"), fabric_map) + # iterate over each SAN and apply connection control + for fabric in fabric_map.keys(): + connected_fabric = fabric + t_list = fabric_map[fabric] + # get valid I-T map to add connection control + i_t_map = {initiator: t_list} + valid_i_t_map = self.get_valid_initiator_target_map( + i_t_map, True) + LOG.info(_("Final filtered map for fabric: %s"), + {fabric: valid_i_t_map}) + + # Call driver to add connection control + self.driver.add_connection(fabric, valid_i_t_map) + + LOG.info(_("Add Connection: Finished iterating " + "over all target list")) + except Exception as e: + msg = _("Failed adding connection for fabric=%(fabric)s: " + "Error:%(err)s") % {'fabric': connected_fabric, + 'err': str(e)} + LOG.error(msg) + raise exception.ZoneManagerException(reason=msg) + + def delete_connection(self, initiator_target_map): + """Delete connection. + + Updates/deletes connection control for the given initiator target map. + initiator_target_map - each initiator WWN mapped to a list of one + or more target WWN: + eg: + { + '10008c7cff523b01': ['20240002ac000a50', '20240002ac000a40'] + } + """ + connected_fabric = None + try: + for initiator in initiator_target_map.keys(): + target_list = initiator_target_map[initiator] + LOG.info(_("Delete connection Target List:%s"), + {initiator: target_list}) + + # get SAN context for the target list + fabric_map = self.driver.get_san_context(target_list) + LOG.debug(_("Delete connection Fabric Map from SAN " + "context: %s"), fabric_map) + + # iterate over each SAN and apply connection control + for fabric in fabric_map.keys(): + connected_fabric = fabric + t_list = fabric_map[fabric] + # get valid I-T map to add connection control + i_t_map = {initiator: t_list} + valid_i_t_map = self.get_valid_initiator_target_map( + i_t_map, False) + LOG.info(_("Final filtered map for delete " + "connection: %s"), valid_i_t_map) + + # Call driver to delete connection control + if len(valid_i_t_map) > 0: + self.driver.delete_connection(fabric, valid_i_t_map) + + LOG.debug(_("Delete Connection - Finished iterating over all" + " target list")) + except Exception as e: + msg = _("Failed removing connection for fabric=%(fabric)s: " + "Error:%(err)s") % {'fabric': connected_fabric, + 'err': str(e)} + LOG.error(msg) + raise exception.ZoneManagerException(reason=msg) + + def get_valid_initiator_target_map(self, initiator_target_map, + add_control): + """Reference count check for end devices. + + Looks up the reference count for each initiator-target pair from the + map and returns a filtered list based on the operation type + add_control - operation type can be true for add connection control + and false for remove connection control + """ + filtered_i_t_map = {} + for initiator in initiator_target_map.keys(): + t_list = initiator_target_map[initiator] + for target in t_list: + count = self.get_zoning_state_ref_count(initiator, target) + if add_control: + if count > 0: + t_list.remove(target) + # update count = count + 1 + else: + if count > 1: + t_list.remove(target) + # update count = count - 1 + if t_list: + filtered_i_t_map[initiator] = t_list + else: + LOG.info(_("No targets to add or remove connection for " + "I: %s"), initiator) + return filtered_i_t_map diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index b7d490124..21452e06e 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -1762,6 +1762,59 @@ # (boolean value) #volume_service_inithost_offload=false +# FC Zoning mode configured (string value) +#zoning_mode=none + + +# +# Options defined in cinder.zonemanager.drivers.brocade.brcd_fc_zone_driver +# + +# Southbound connector for zoning operation (string value) +#brcd_sb_connector=cinder.zonemanager.drivers.brocade.brcd_fc_zone_client_cli.BrcdFCZoneClientCLI + + +# +# Options defined in cinder.zonemanager.drivers.fc_common +# + +# Comma separated list of fibre channel fabric names. This +# list of names is used to retrieve other SAN credentials for +# connecting to each SAN fabric (string value) +#fc_fabric_names= + + +# +# Options defined in cinder.zonemanager.drivers.fc_zone_driver +# + +# Indicates whether zone should be activated or not (boolean +# value) +#zone_activate=true + +# A prefix to be used when naming zone (string value) +#zone_name_prefix=openstack + + +# +# Options defined in cinder.zonemanager.fc_san_lookup_service +# + +# FC San Lookup Service (string value) +#fc_san_lookup_service=cinder.zonemanager.drivers.brocade.brcd_fc_san_lookup_service.BrcdFCSanLookupService + + +# +# Options defined in cinder.zonemanager.fc_zone_manager +# + +# FC Zone Driver responsible for zone management (string +# value) +#zone_driver=cinder.zonemanager.drivers.brocade.brcd_fc_zone_driver.BrcdFCZoneDriver + +# Zoning policy configured by user (string value) +#zoning_policy=initiator-target + [ssl]