From: Santhoshkumar Kolathur Date: Sat, 31 Aug 2013 02:48:52 +0000 (-0700) Subject: Add support for FC zone lifecycle management X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=d8fcf93c91c856c3e895a85df6fb8fddac8e715d;p=openstack-build%2Fcinder-build.git Add support for FC zone lifecycle management Allows automated zone lifecycle management in the attach/detach entry points of the volume manager for FC volumes (when fabric zoning is enabled). Zones are configured automatically as part of the active zone set in the FC SAN to provide a more flexible and secure way of controlling access. FC zone manager enables SAN vendors to add support for pluggable implementations. Change-Id: I86e02e11afec71ffa25dfbcbd0dc3ddcc35741ea Implements: blueprint cinder-fc-zone-manager --- 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]