From 935aa1a5b401d086334fa8ac52bf01170a3eb9ca Mon Sep 17 00:00:00 2001 From: Pradeep Sathasivam Date: Thu, 21 May 2015 20:31:29 +0530 Subject: [PATCH] Adds HTTPS southbound connector for Brocade FC Zone Driver Set ssl verify to False for HTTPS. Marked 'principal_switch_wwn' parameter from the config options as deprecated. DocImpact Implements: blueprint brocade-zone-driver-virtualfabrics-support Change-Id: I0b40b520580eaa6821c0af29abc6d2497d884ad2 --- cinder/exception.py | 8 +- .../test_brcd_fc_san_lookup_service.py | 9 - .../test_brcd_fc_zone_client_cli.py | 26 +- .../zonemanager/test_brcd_fc_zone_driver.py | 83 +- .../test_brcd_http_fc_zone_client.py | 585 ++++++++++++++ .../zonemanager/test_brcd_lookup_service.py | 3 - .../drivers/brocade/brcd_fabric_opts.py | 27 +- .../brocade/brcd_fc_san_lookup_service.py | 13 +- .../brocade/brcd_fc_zone_client_cli.py | 61 +- .../brocade/brcd_fc_zone_connector_factory.py | 82 ++ .../drivers/brocade/brcd_fc_zone_driver.py | 123 ++- .../brocade/brcd_http_fc_zone_client.py | 734 ++++++++++++++++++ .../drivers/brocade/fc_zone_constants.py | 59 +- ...ocade_http_connector-0021e41dfa56e671.yaml | 10 + 14 files changed, 1657 insertions(+), 166 deletions(-) create mode 100644 cinder/tests/unit/zonemanager/test_brcd_http_fc_zone_client.py create mode 100644 cinder/zonemanager/drivers/brocade/brcd_fc_zone_connector_factory.py create mode 100644 cinder/zonemanager/drivers/brocade/brcd_http_fc_zone_client.py create mode 100644 releasenotes/notes/brocade_http_connector-0021e41dfa56e671.yaml diff --git a/cinder/exception.py b/cinder/exception.py index 1911a57e8..88b3c3da9 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -842,11 +842,15 @@ class FCSanLookupServiceException(CinderException): class BrocadeZoningCliException(CinderException): - message = _("Fibre Channel Zoning CLI error: %(reason)s") + message = _("Brocade Fibre Channel Zoning CLI error: %(reason)s") + + +class BrocadeZoningHttpException(CinderException): + message = _("Brocade Fibre Channel Zoning HTTP error: %(reason)s") class CiscoZoningCliException(CinderException): - message = _("Fibre Channel Zoning CLI error: %(reason)s") + message = _("Cisco Fibre Channel Zoning CLI error: %(reason)s") class NetAppDriverException(VolumeDriverException): diff --git a/cinder/tests/unit/zonemanager/test_brcd_fc_san_lookup_service.py b/cinder/tests/unit/zonemanager/test_brcd_fc_san_lookup_service.py index 4598fec30..987980a76 100644 --- a/cinder/tests/unit/zonemanager/test_brcd_fc_san_lookup_service.py +++ b/cinder/tests/unit/zonemanager/test_brcd_fc_san_lookup_service.py @@ -1,8 +1,6 @@ # (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 @@ -143,10 +141,3 @@ class TestBrcdFCSanLookupService(brcd_lookup.BrcdFCSanLookupService, self.assertEqual(parsed_switch_port_wwns, return_wwn_list) self.assertRaises(exception.InvalidParameterValue, self._parse_ns_output, invalid_switch_data) - - def test_get_formatted_wwn(self): - wwn_list = ['10008c7cff523b01'] - return_wwn_list = [] - expected_wwn_list = ['10:00:8c:7c:ff:52:3b:01'] - return_wwn_list.append(self.get_formatted_wwn(wwn_list[0])) - self.assertEqual(expected_wwn_list, return_wwn_list) diff --git a/cinder/tests/unit/zonemanager/test_brcd_fc_zone_client_cli.py b/cinder/tests/unit/zonemanager/test_brcd_fc_zone_client_cli.py index b60306781..94e9ece90 100644 --- a/cinder/tests/unit/zonemanager/test_brcd_fc_zone_client_cli.py +++ b/cinder/tests/unit/zonemanager/test_brcd_fc_zone_client_cli.py @@ -1,8 +1,6 @@ # (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 @@ -24,9 +22,9 @@ from oslo_concurrency import processutils from cinder import exception from cinder import test -from cinder.zonemanager.drivers.brocade \ - import brcd_fc_zone_client_cli as client_cli -import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant +from cinder.zonemanager.drivers.brocade import (brcd_fc_zone_client_cli + as client_cli) +import cinder.zonemanager.drivers.brocade.fc_zone_constants as zone_constant nsshow = '20:1a:00:05:1e:e8:e3:29' @@ -78,7 +76,7 @@ class TestBrcdFCZoneClientCLI(client_cli.BrcdFCZoneClientCLI, test.TestCase): @mock.patch.object(client_cli.BrcdFCZoneClientCLI, '_get_switch_info') def test_get_active_zone_set(self, get_switch_info_mock): - cmd_list = [ZoneConstant.GET_ACTIVE_ZONE_CFG] + cmd_list = [zone_constant.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) @@ -178,7 +176,6 @@ class TestBrcdFCZoneClientCLI(client_cli.BrcdFCZoneClientCLI, test.TestCase): @mock.patch.object(client_cli.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'] get_switch_info_mock.return_value = (switch_data) ns_info_list = self.get_nameserver_info() @@ -192,7 +189,7 @@ class TestBrcdFCZoneClientCLI(client_cli.BrcdFCZoneClientCLI, test.TestCase): @mock.patch.object(client_cli.BrcdFCZoneClientCLI, '_ssh_execute') def test__cfg_save(self, ssh_execute_mock): - cmd_list = [ZoneConstant.CFG_SAVE] + cmd_list = [zone_constant.CFG_SAVE] self._cfg_save() ssh_execute_mock.assert_called_once_with(cmd_list, True, 1) @@ -205,7 +202,7 @@ class TestBrcdFCZoneClientCLI(client_cli.BrcdFCZoneClientCLI, test.TestCase): @mock.patch.object(client_cli.BrcdFCZoneClientCLI, 'apply_zone_change') def test__cfg_trans_abort(self, apply_zone_change_mock): - cmd_list = [ZoneConstant.CFG_ZONE_TRANS_ABORT] + cmd_list = [zone_constant.CFG_ZONE_TRANS_ABORT] with mock.patch.object(self, '_is_trans_abortable') \ as is_trans_abortable_mock: is_trans_abortable_mock.return_value = True @@ -215,8 +212,8 @@ class TestBrcdFCZoneClientCLI(client_cli.BrcdFCZoneClientCLI, test.TestCase): @mock.patch.object(client_cli.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), + cmd_list = [zone_constant.CFG_SHOW_TRANS] + run_ssh_mock.return_value = (Stream(zone_constant.TRANS_ABORTABLE), None) data = self._is_trans_abortable() self.assertTrue(data) @@ -230,7 +227,7 @@ class TestBrcdFCZoneClientCLI(client_cli.BrcdFCZoneClientCLI, test.TestCase): @mock.patch.object(client_cli.BrcdFCZoneClientCLI, '_run_ssh') def test__is_trans_abortable_false(self, run_ssh_mock): - cmd_list = [ZoneConstant.CFG_SHOW_TRANS] + cmd_list = [zone_constant.CFG_SHOW_TRANS] cfgtransshow = 'There is no outstanding zoning transaction' run_ssh_mock.return_value = (Stream(cfgtransshow), None) data = self._is_trans_abortable() @@ -239,14 +236,14 @@ class TestBrcdFCZoneClientCLI(client_cli.BrcdFCZoneClientCLI, test.TestCase): @mock.patch.object(client_cli.BrcdFCZoneClientCLI, '_run_ssh') def test_apply_zone_change(self, run_ssh_mock): - cmd_list = [ZoneConstant.CFG_SAVE] + cmd_list = [zone_constant.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) @mock.patch.object(client_cli.BrcdFCZoneClientCLI, '_run_ssh') def test__get_switch_info(self, run_ssh_mock): - cmd_list = [ZoneConstant.NS_SHOW] + cmd_list = [zone_constant.NS_SHOW] nsshow_list = [nsshow] run_ssh_mock.return_value = (Stream(nsshow), Stream()) switch_data = self._get_switch_info(cmd_list) @@ -255,7 +252,6 @@ class TestBrcdFCZoneClientCLI(client_cli.BrcdFCZoneClientCLI, test.TestCase): 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(expected_wwn_list, return_wwn_list) diff --git a/cinder/tests/unit/zonemanager/test_brcd_fc_zone_driver.py b/cinder/tests/unit/zonemanager/test_brcd_fc_zone_driver.py index 460b4b595..e42543dc0 100644 --- a/cinder/tests/unit/zonemanager/test_brcd_fc_zone_driver.py +++ b/cinder/tests/unit/zonemanager/test_brcd_fc_zone_driver.py @@ -1,8 +1,6 @@ # (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 @@ -23,6 +21,7 @@ import mock from oslo_config import cfg from oslo_utils import importutils import paramiko +import requests from cinder import exception from cinder import test @@ -76,15 +75,16 @@ class BrcdFcZoneDriverBaseTest(object): configuration.fc_fabric_names = 'BRCD_FAB_1' configuration.fc_fabric_address_BRCD_FAB_1 = '10.24.48.213' - if (is_normal): + configuration.fc_southbound_connector = 'CLI' + 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): + if mode == 1: configuration.zoning_policy_BRCD_FAB_1 = 'initiator-target' - elif (mode == 2): + elif mode == 2: configuration.zoning_policy_BRCD_FAB_1 = 'initiator' else: configuration.zoning_policy_BRCD_FAB_1 = 'initiator-target' @@ -110,40 +110,60 @@ class TestBrcdFcZoneDriver(BrcdFcZoneDriverBaseTest, test.TestCase): def fake__get_active_zone_set(self, brcd_sb_connector, fabric_ip): return GlobalVars._active_cfg + def get_client(self, protocol='HTTPS'): + conn = ('cinder.tests.unit.zonemanager.test_brcd_fc_zone_driver.' + + ('FakeBrcdFCZoneClientCLI' if protocol == "CLI" + else 'FakeBrcdHttpFCZoneClient')) + client = importutils.import_object( + conn, + ipaddress="10.24.48.213", + username="admin", + password="password", + key="/home/stack/.ssh/id_rsa", + port=22, + protocol=protocol + ) + return client + def fake_get_san_context(self, target_wwn_list): fabric_map = {} return fabric_map - @mock.patch.object(driver.BrcdFCZoneDriver, '_get_active_zone_set') - def test_add_connection(self, get_active_zs_mock): + @mock.patch.object(driver.BrcdFCZoneDriver, '_get_southbound_client') + def test_add_connection(self, get_southbound_client_mock): """Normal flow for i-t mode.""" GlobalVars._is_normal_test = True GlobalVars._zone_state = [] - get_active_zs_mock.return_value = _active_cfg_before_add + GlobalVars._active_cfg = _active_cfg_before_add + get_southbound_client_mock.return_value = self.get_client("HTTPS") self.driver.add_connection('BRCD_FAB_1', _initiator_target_map) self.assertTrue(_zone_name in GlobalVars._zone_state) - @mock.patch.object(driver.BrcdFCZoneDriver, '_get_active_zone_set') - def test_delete_connection(self, get_active_zs_mock): + @mock.patch.object(driver.BrcdFCZoneDriver, '_get_southbound_client') + def test_delete_connection(self, get_southbound_client_mock): GlobalVars._is_normal_test = True - get_active_zs_mock.return_value = _active_cfg_before_delete + get_southbound_client_mock.return_value = self.get_client("CLI") + 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) - @mock.patch.object(driver.BrcdFCZoneDriver, '_get_active_zone_set') - def test_add_connection_for_initiator_mode(self, get_active_zs_mock): + @mock.patch.object(driver.BrcdFCZoneDriver, '_get_southbound_client') + def test_add_connection_for_initiator_mode(self, get_southbound_client_mk): """Normal flow for i mode.""" GlobalVars._is_normal_test = True - get_active_zs_mock.return_value = _active_cfg_before_add + get_southbound_client_mk.return_value = self.get_client("CLI") + 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) - @mock.patch.object(driver.BrcdFCZoneDriver, '_get_active_zone_set') - def test_delete_connection_for_initiator_mode(self, get_active_zs_mock): + @mock.patch.object(driver.BrcdFCZoneDriver, '_get_southbound_client') + def test_delete_connection_for_initiator_mode(self, + get_southbound_client_mk): GlobalVars._is_normal_test = True - get_active_zs_mock.return_value = _active_cfg_before_delete + get_southbound_client_mk.return_value = self.get_client("HTTPS") + 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) @@ -170,12 +190,7 @@ class TestBrcdFcZoneDriver(BrcdFcZoneDriverBaseTest, test.TestCase): _initiator_target_map) -class FakeBrcdFCZoneClientCLI(object): - def __init__(self, ipaddress, username, password, port): - self.firmware_supported = True - if not GlobalVars._is_normal_test: - raise paramiko.SSHException("Unable to connect to fabric") - +class FakeClient(object): def get_active_zone_set(self): return GlobalVars._active_cfg @@ -200,7 +215,25 @@ class FakeBrcdFCZoneClientCLI(object): pass +class FakeBrcdFCZoneClientCLI(FakeClient): + def __init__(self, ipaddress, username, + password, port, key, protocol): + self.firmware_supported = True + if not GlobalVars._is_normal_test: + raise paramiko.SSHException("Unable to connect to fabric.") + + +class FakeBrcdHttpFCZoneClient(FakeClient): + + def __init__(self, ipaddress, username, + password, port, key, protocol): + self.firmware_supported = True + if not GlobalVars._is_normal_test: + raise requests.exception.HTTPError("Unable to connect to fabric") + + class FakeBrcdFCSanLookupService(object): + def get_device_mapping_from_network(self, initiator_wwn_list, target_wwn_list): @@ -208,10 +241,10 @@ class FakeBrcdFCSanLookupService(object): initiators = [] targets = [] for i in initiator_wwn_list: - if (i in _initiator_ns_map[_fabric_wwn]): + 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]): + if t in _target_ns_map[_fabric_wwn]: targets.append(t) device_map[_fabric_wwn] = { 'initiator_port_wwn_list': initiators, diff --git a/cinder/tests/unit/zonemanager/test_brcd_http_fc_zone_client.py b/cinder/tests/unit/zonemanager/test_brcd_http_fc_zone_client.py new file mode 100644 index 000000000..67872f03b --- /dev/null +++ b/cinder/tests/unit/zonemanager/test_brcd_http_fc_zone_client.py @@ -0,0 +1,585 @@ +# (c) Copyright 2015 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Unit tests for brcd fc zone client http(s).""" +from mock import patch + +from cinder import exception +from cinder import test +from cinder.zonemanager.drivers.brocade import (brcd_http_fc_zone_client + as client) +import cinder.zonemanager.drivers.brocade.fc_zone_constants as zone_constant + + +cfgs = {'openstack_cfg': 'zone1;zone2'} +cfgs_to_delete = { + 'openstack_cfg': 'zone1;zone2;openstack50060b0000c26604201900051ee8e329'} +zones = {'zone1': '20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11', + 'zone2': '20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11'} + +zones_to_delete = { + 'zone1': '20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11', + 'zone2': '20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11', + 'openstack50060b0000c26604201900051ee8e329': + '50:06:0b:00:00:c2:66:04;20:19:00:05:1e:e8:e3:29'} + +alias = {} +qlps = {} +ifas = {} +parsed_raw_zoneinfo = "" +random_no = '' +session = None +active_cfg = 'openstack_cfg' +activate = True +no_activate = False +ns_info = ['10:00:00:05:1e:7c:64:96'] +nameserver_info = """ + + + + +NSInfo Page + + +
+--BEGIN NS INFO
+
+2;8;020800;N    ;10:00:00:05:1e:7c:64:96;20:00:00:05:1e:7c:64:96;[89]""" \
+"""Brocade-825 | 3.0.4.09 | DCM-X3650-94 | Microsoft Windows Server 2003 R2"""\
+    """| Service Pack 2";FCP ;      3;20:08:00:05:1e:89:54:a0;"""\
+    """0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0;000000;port8"""\
+    """
+--END NS INFO
+
+
+ + +""" +mocked_zone_string = 'zonecfginfo=openstack_cfg zone1;zone2 '\ + 'zone2 20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11 '\ + 'zone1 20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11 '\ + 'alia1 10:00:00:05:1e:7c:64:96;10:21:10:05:33:0e:96:12 '\ + 'qlp 10:11:f4:ce:46:ae:68:6c;20:11:f4:ce:46:ae:68:6c '\ + 'fa1 20:15:f4:ce:96:ae:68:6c;20:11:f4:ce:46:ae:68:6c '\ + 'openstack_cfg null &saveonly=false' +mocked_zone_string_no_activate = 'zonecfginfo=openstack_cfg zone1;zone2 '\ + 'zone2 20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11 '\ + 'zone1 20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11 '\ + 'alia1 10:00:00:05:1e:7c:64:96;10:21:10:05:33:0e:96:12 '\ + 'qlp 10:11:f4:ce:46:ae:68:6c;20:11:f4:ce:46:ae:68:6c '\ + 'fa1 20:15:f4:ce:96:ae:68:6c;20:11:f4:ce:46:ae:68:6c &saveonly=true' +zone_string_to_post = "zonecfginfo=openstack_cfg "\ + "openstack50060b0000c26604201900051ee8e329;zone1;zone2 "\ + "zone2 20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11 "\ + "zone1 20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11 "\ + "openstack50060b0000c26604201900051ee8e329 "\ + "50:06:0b:00:00:c2:66:04;20:19:00:05:1e:e8:e3:29 "\ + "openstack_cfg null &saveonly=false" +zone_string_to_post_no_activate = "zonecfginfo=openstack_cfg "\ + "openstack50060b0000c26604201900051ee8e329;zone1;zone2 "\ + "zone2 20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11 "\ + "zone1 20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11 "\ + "openstack50060b0000c26604201900051ee8e329 "\ + "50:06:0b:00:00:c2:66:04;20:19:00:05:1e:e8:e3:29 &saveonly=true" +zone_string_to_post_invalid_request = "zonecfginfo=openstack_cfg "\ + "openstack50060b0000c26604201900051ee8e32900000000000000000000000000;"\ + "zone1;zone2 openstack50060b0000c26604201900051ee8e329000000000000000000000"\ + "00000 50:06:0b:00:00:c2:66:04;20:19:00:05:1e:e8:e3:29 "\ + "zone1 20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11 "\ + "zone2 20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11 &saveonly=true" +zone_string_del_to_post = "zonecfginfo=openstack_cfg zone1;zone2"\ + " zone2 20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11 "\ + "zone1 20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11 "\ + "openstack_cfg null &saveonly=false" +zone_string_del_to_post_no_active = "zonecfginfo=openstack_cfg zone1;zone2"\ + " zone2 20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11 "\ + "zone1 20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11 &saveonly=true" +zone_post_page = """ + +
+--BEGIN ZONE_TXN_INFO
+txnId=34666
+adId=0
+user=admin
+roleUser=admin
+openTxnOwner=
+openTxnId=0
+openTxnAbortable=0
+txnStarttime=1421916354
+txnEndtime=1421916355
+currStateInt=4
+prevStateInt=3
+actionInt=5
+currState=done
+prevState=progress
+action=error
+sessionId=5892021
+selfAborted=false
+status=done
+errorCode=-1
+errorMessage=Name too long
+--END ZONE_TXN_INFO
+
+""" +zone_post_page_no_error = """ + +
+--BEGIN ZONE_TXN_INFO
+txnId=34666
+adId=0
+user=admin
+roleUser=admin
+openTxnOwner=
+openTxnId=0
+openTxnAbortable=0
+txnStarttime=1421916354
+txnEndtime=1421916355
+currStateInt=4
+prevStateInt=3
+actionInt=5
+currState=done
+prevState=progress
+action=error
+sessionId=5892021
+selfAborted=false
+status=done
+errorCode=0
+errorMessage=
+--END ZONE_TXN_INFO
+
+""" +secinfo_resp = """ + +
+--BEGIN SECINFO
+SECURITY = OFF
+RANDOM = 6281590
+DefaultPasswdBitmap = 0
+primaryFCS = no
+switchType = 66
+resource = 10.24.48.210
+REALM = FC Switch Administration
+AUTHMETHOD = Custom_Basic
+hasUpfrontLogin=yes
+AUTHVERSION = 1
+vfEnabled=false
+vfSupported=true
+--END SECINFO
+
+ +""" +authenticate_resp = """ +
+--BEGIN AUTHENTICATE
+authenticated = yes
+username=admin
+userrole=admin
+adCapable=1
+currentAD=AD0
+trueADEnvironment=0
+adId=0
+adList=ALL
+contextType=0
+--END AUTHENTICATE
+
+ +""" +un_authenticate_resp = """ + + + +Authentication + + +
+--BEGIN AUTHENTICATE
+authenticated = no
+errCode = -3
+authType = Custom_Basic
+realm = FC Switch Administration
+--END AUTHENTICATE
+
+ +""" +switch_page_resp = """ + + + + + +
+--BEGIN SWITCH INFORMATION
+didOffset=96
+swFWVersion=v7.3.0b_rc1_bld06
+swDomain=2
+--END SWITCH INFORMATION
+
+ + +""" +switch_page_invalid_firm = """ + + + + + +
+--BEGIN SWITCH INFORMATION
+didOffset=96
+swFWVersion=v6.1.1
+swDomain=2
+--END SWITCH INFORMATION
+
+ + +""" +parsed_value = """ +didOffset=96 +swFWVersion=v7.3.0b_rc1_bld06 +swDomain=2 +""" +zone_info = """ + + + +Zone Configuration Information + + +
+--BEGIN ZONE CHANGE
+LastZoneChangeTime=1421926251
+--END ZONE CHANGE
+isZoneTxnSupported=true
+ZoneLicense=true
+QuickLoopLicense=true
+DefZoneStatus=noaccess
+McDataDefaultZone=false
+McDataSafeZone=false
+AvailableZoneSize=1043890
+--BEGIN ZONE INFO
+openstack_cfg zone1;zone2 """\
+"""zone1 20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11 """\
+    """zone2 20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11 """\
+    """alia1 10:00:00:05:1e:7c:64:96;10:21:10:05:33:0e:96:12 """\
+    """qlp 10:11:f4:ce:46:ae:68:6c;20:11:f4:ce:46:ae:68:6c """\
+    """fa1 20:15:f4:ce:96:ae:68:6c;20:11:f4:ce:46:ae:68:6c """\
+    """openstack_cfg null 1045274"""\
+    """--END ZONE INFO
+
+ + + +""" + +active_zone_set = { + 'zones': + {'zone1': + ['20:01:00:05:33:0e:96:15', '20:00:00:05:33:0e:93:11'], + 'zone2': + ['20:01:00:05:33:0e:96:14', '20:00:00:05:33:0e:93:11']}, + 'active_zone_config': 'openstack_cfg'} +updated_zones = {'zone1': '20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11', + 'zone2': '20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11', + 'test_updated_zone': + '20:01:00:05:33:0e:96:10;20:00:00:05:33:0e:93:11'} +updated_cfgs = {'openstack_cfg': 'test_updated_zone;zone1;zone2'} +valid_zone_name = "openstack50060b0000c26604201900051ee8e329" + + +class TestBrcdHttpFCZoneClient(client.BrcdHTTPFCZoneClient, test.TestCase): + + def setUp(self): + self.auth_header = "YWRtaW46cGFzc3dvcmQ6NDM4ODEyNTIw" + self.switch_user = "admin" + self.switch_pwd = "password" + self.protocol = "HTTPS" + self.conn = None + self.alias = {} + self.qlps = {} + self.ifas = {} + self.parsed_raw_zoneinfo = "" + self.random_no = '' + self.session = None + super(TestBrcdHttpFCZoneClient, self).setUp() + + # override some of the functions + def __init__(self, *args, **kwargs): + test.TestCase.__init__(self, *args, **kwargs) + + @patch.object(client.BrcdHTTPFCZoneClient, 'connect') + def test_create_auth_token(self, connect_mock): + connect_mock.return_value = secinfo_resp + self.assertEqual("Custom_Basic YWRtaW46cGFzc3dvcmQ6NjI4MTU5MA==", + self.create_auth_token()) + + @patch.object(client.BrcdHTTPFCZoneClient, 'connect') + def test_authenticate(self, connect_mock): + connect_mock.return_value = authenticate_resp + self.assertEqual( + (True, "Custom_Basic YWRtaW46eHh4Og=="), self.authenticate()) + + @patch.object(client.BrcdHTTPFCZoneClient, 'connect') + def test_authenticate_failed(self, connect_mock): + connect_mock.return_value = un_authenticate_resp + self.assertRaises( + exception.BrocadeZoningHttpException, self.authenticate) + + def test_get_parsed_data(self): + valid_delimiter1 = zone_constant.SWITCHINFO_BEGIN + valid_delimiter2 = zone_constant.SWITCHINFO_END + invalid_delimiter = "--END SWITCH INFORMATION1" + self.assertEqual(parsed_value, self.get_parsed_data( + switch_page_resp, valid_delimiter1, valid_delimiter2)) + self.assertRaises(exception.BrocadeZoningHttpException, + self.get_parsed_data, + switch_page_resp, + valid_delimiter1, + invalid_delimiter) + self.assertRaises(exception.BrocadeZoningHttpException, + self.get_parsed_data, + switch_page_resp, + invalid_delimiter, + valid_delimiter2) + + def test_get_nvp_value(self): + valid_keyname = zone_constant.FIRMWARE_VERSION + invalid_keyname = "swFWVersion1" + self.assertEqual( + "v7.3.0b_rc1_bld06", self.get_nvp_value(parsed_value, + valid_keyname)) + self.assertRaises(exception.BrocadeZoningHttpException, + self.get_nvp_value, + parsed_value, + invalid_keyname) + + @patch.object(client.BrcdHTTPFCZoneClient, 'connect') + def test_is_supported_firmware(self, connect_mock): + connect_mock.return_value = switch_page_resp + self.assertTrue(self.is_supported_firmware()) + + @patch.object(client.BrcdHTTPFCZoneClient, 'connect') + def test_is_supported_firmware_invalid(self, connect_mock): + connect_mock.return_value = switch_page_invalid_firm + self.assertFalse(self.is_supported_firmware()) + + @patch.object(client.BrcdHTTPFCZoneClient, 'connect') + def test_get_active_zone_set(self, connect_mock): + connect_mock.return_value = zone_info + returned_zone_map = self.get_active_zone_set() + self.assertDictMatch(active_zone_set, returned_zone_map) + + def test_form_zone_string(self): + new_alias = { + 'alia1': '10:00:00:05:1e:7c:64:96;10:21:10:05:33:0e:96:12'} + new_qlps = {'qlp': '10:11:f4:ce:46:ae:68:6c;20:11:f4:ce:46:ae:68:6c'} + new_ifas = {'fa1': '20:15:f4:ce:96:ae:68:6c;20:11:f4:ce:46:ae:68:6c'} + self.assertEqual(mocked_zone_string, self.form_zone_string( + cfgs, active_cfg, zones, new_alias, new_qlps, new_ifas, True)) + self.assertEqual(mocked_zone_string_no_activate, self.form_zone_string( + cfgs, active_cfg, zones, new_alias, new_qlps, new_ifas, False)) + + @patch.object(client.BrcdHTTPFCZoneClient, 'post_zone_data') + def test_add_zones_activate(self, post_zone_data_mock): + post_zone_data_mock.return_value = ("0", "") + self.cfgs = cfgs.copy() + self.zones = zones.copy() + self.alias = alias.copy() + self.qlps = qlps.copy() + self.ifas = ifas.copy() + self.active_cfg = active_cfg + add_zones_info = {valid_zone_name: + ['50:06:0b:00:00:c2:66:04', + '20:19:00:05:1e:e8:e3:29'] + } + self.add_zones(add_zones_info, True) + post_zone_data_mock.assert_called_once_with(zone_string_to_post) + + @patch.object(client.BrcdHTTPFCZoneClient, 'post_zone_data') + def test_add_zones_invalid_zone_name(self, post_zone_data_mock): + post_zone_data_mock.return_value = ("-1", "Name Too Long") + self.cfgs = cfgs.copy() + self.zones = zones.copy() + self.alias = alias.copy() + self.qlps = qlps.copy() + self.ifas = ifas.copy() + self.active_cfg = active_cfg + invalid_zone_name = valid_zone_name + "00000000000000000000000000" + add_zones_info = {invalid_zone_name: + ['50:06:0b:00:00:c2:66:04', + '20:19:00:05:1e:e8:e3:29'] + } + self.assertRaises( + exception.BrocadeZoningHttpException, + self.add_zones, add_zones_info, False) + + @patch.object(client.BrcdHTTPFCZoneClient, 'post_zone_data') + def test_add_zones_no_activate(self, post_zone_data_mock): + post_zone_data_mock.return_value = ("0", "") + self.cfgs = cfgs.copy() + self.zones = zones.copy() + self.alias = alias.copy() + self.qlps = qlps.copy() + self.ifas = ifas.copy() + self.active_cfg = active_cfg + add_zones_info = {valid_zone_name: + ['50:06:0b:00:00:c2:66:04', + '20:19:00:05:1e:e8:e3:29'] + } + self.add_zones(add_zones_info, False) + post_zone_data_mock.assert_called_once_with( + zone_string_to_post_no_activate) + + @patch.object(client.BrcdHTTPFCZoneClient, 'post_zone_data') + def test_delete_zones_activate(self, post_zone_data_mock): + post_zone_data_mock.return_value = ("0", "") + self.cfgs = cfgs_to_delete.copy() + self.zones = zones_to_delete.copy() + self.alias = alias.copy() + self.qlps = qlps.copy() + self.ifas = ifas.copy() + self.active_cfg = active_cfg + delete_zones_info = valid_zone_name + + self.delete_zones(delete_zones_info, True) + post_zone_data_mock.assert_called_once_with(zone_string_del_to_post) + + @patch.object(client.BrcdHTTPFCZoneClient, 'post_zone_data') + def test_delete_zones_no_activate(self, post_zone_data_mock): + post_zone_data_mock.return_value = ("0", "") + self.cfgs = cfgs_to_delete.copy() + self.zones = zones_to_delete.copy() + self.alias = alias.copy() + self.qlps = qlps.copy() + self.ifas = ifas.copy() + self.active_cfg = active_cfg + delete_zones_info = valid_zone_name + self.delete_zones(delete_zones_info, False) + post_zone_data_mock.assert_called_once_with( + zone_string_del_to_post_no_active) + + @patch.object(client.BrcdHTTPFCZoneClient, 'post_zone_data') + def test_delete_zones_invalid_zone_name(self, post_zone_data_mock): + post_zone_data_mock.return_value = ("0", "") + self.cfgs = cfgs_to_delete.copy() + self.zones = zones_to_delete.copy() + self.alias = alias.copy() + self.qlps = qlps.copy() + self.ifas = ifas.copy() + self.active_cfg = active_cfg + delete_zones_info = 'openstack50060b0000c26604201900051ee8e32' + self.assertRaises(exception.BrocadeZoningHttpException, + self.delete_zones, delete_zones_info, False) + + @patch.object(client.BrcdHTTPFCZoneClient, 'connect') + def test_post_zone_data(self, connect_mock): + connect_mock.return_value = zone_post_page + self.assertEqual( + ("-1", "Name too long"), self.post_zone_data(zone_string_to_post)) + connect_mock.return_value = zone_post_page_no_error + self.assertEqual(("0", ""), self.post_zone_data(zone_string_to_post)) + + @patch.object(client.BrcdHTTPFCZoneClient, 'connect') + def test_get_nameserver_info(self, connect_mock): + connect_mock.return_value = nameserver_info + self.assertEqual(ns_info, self.get_nameserver_info()) + + def test_delete_update_zones_cfgs(self): + + cfgs = {'openstack_cfg': 'zone1;zone2'} + zones = {'zone1': '20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11', + 'zone2': '20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11'} + delete_zones_info = valid_zone_name + self.assertEqual( + (zones, cfgs, active_cfg), + self.delete_update_zones_cfgs( + cfgs_to_delete.copy(), + zones_to_delete.copy(), + delete_zones_info, + active_cfg)) + + cfgs = {'openstack_cfg': 'zone2'} + zones = {'zone2': '20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11'} + delete_zones_info = valid_zone_name + ";zone1" + self.assertEqual( + (zones, cfgs, active_cfg), + self.delete_update_zones_cfgs( + cfgs_to_delete.copy(), + zones_to_delete.copy(), + delete_zones_info, + active_cfg)) + + def test_add_update_zones_cfgs(self): + add_zones_info = {valid_zone_name: + ['50:06:0b:00:00:c2:66:04', + '20:19:00:05:1e:e8:e3:29'] + } + updated_cfgs = { + 'openstack_cfg': + valid_zone_name + ';zone1;zone2'} + updated_zones = { + 'zone1': '20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11', + 'zone2': '20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11', + valid_zone_name: + '50:06:0b:00:00:c2:66:04;20:19:00:05:1e:e8:e3:29'} + self.assertEqual((updated_zones, updated_cfgs, active_cfg), + self.add_update_zones_cfgs( + cfgs.copy(), + zones.copy(), + add_zones_info, + active_cfg, + "openstack_cfg")) + + add_zones_info = {valid_zone_name: + ['50:06:0b:00:00:c2:66:04', + '20:19:00:05:1e:e8:e3:29'], + 'test4': + ['20:06:0b:00:00:b2:66:07', + '20:10:00:05:1e:b8:c3:19'] + } + updated_cfgs = { + 'openstack_cfg': + 'test4;openstack50060b0000c26604201900051ee8e329;zone1;zone2'} + updated_zones = { + 'zone1': '20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11', + 'zone2': '20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11', + valid_zone_name: + '50:06:0b:00:00:c2:66:04;20:19:00:05:1e:e8:e3:29', + 'test4': '20:06:0b:00:00:b2:66:07;20:10:00:05:1e:b8:c3:19'} + self.assertEqual( + (updated_zones, updated_cfgs, active_cfg), + self.add_update_zones_cfgs( + cfgs.copy(), zones.copy(), add_zones_info, + active_cfg, "openstack_cfg")) + + @patch.object(client.BrcdHTTPFCZoneClient, 'connect') + def test_get_zone_info(self, connect_mock): + connect_mock.return_value = zone_info + self.get_zone_info() + self.assertEqual({'openstack_cfg': 'zone1;zone2'}, self.cfgs) + self.assertEqual( + {'zone1': '20:01:00:05:33:0e:96:15;20:00:00:05:33:0e:93:11', + 'zone2': '20:01:00:05:33:0e:96:14;20:00:00:05:33:0e:93:11'}, + self.zones) + self.assertEqual('openstack_cfg', self.active_cfg) + self.assertEqual( + {'alia1': '10:00:00:05:1e:7c:64:96;10:21:10:05:33:0e:96:12'}, + self.alias) + self.assertEqual( + {'fa1': '20:15:f4:ce:96:ae:68:6c;20:11:f4:ce:46:ae:68:6c'}, + self.ifas) + self.assertEqual( + {'qlp': '10:11:f4:ce:46:ae:68:6c;20:11:f4:ce:46:ae:68:6c'}, + self.qlps) diff --git a/cinder/tests/unit/zonemanager/test_brcd_lookup_service.py b/cinder/tests/unit/zonemanager/test_brcd_lookup_service.py index 8a5d076f4..903482a8f 100644 --- a/cinder/tests/unit/zonemanager/test_brcd_lookup_service.py +++ b/cinder/tests/unit/zonemanager/test_brcd_lookup_service.py @@ -1,8 +1,6 @@ # (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 @@ -16,7 +14,6 @@ # under the License. # - """Unit tests for fc san lookup service.""" from cinder import exception diff --git a/cinder/zonemanager/drivers/brocade/brcd_fabric_opts.py b/cinder/zonemanager/drivers/brocade/brcd_fabric_opts.py index c6bb34827..7cb11f73b 100644 --- a/cinder/zonemanager/drivers/brocade/brcd_fabric_opts.py +++ b/cinder/zonemanager/drivers/brocade/brcd_fabric_opts.py @@ -1,8 +1,6 @@ # (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 @@ -21,30 +19,40 @@ from oslo_log import log as logging from cinder.volume import configuration brcd_zone_opts = [ + cfg.StrOpt('fc_southbound_protocol', + default='HTTP', + choices=('SSH', 'HTTP', 'HTTPS'), + help='South bound connector for the fabric.'), cfg.StrOpt('fc_fabric_address', default='', - help='Management IP of fabric'), + help='Management IP of fabric.'), cfg.StrOpt('fc_fabric_user', default='', - help='Fabric user ID'), + help='Fabric user ID.'), cfg.StrOpt('fc_fabric_password', default='', - help='Password for user', + help='Password for user.', secret=True), cfg.PortOpt('fc_fabric_port', default=22, help='Connecting port'), + cfg.StrOpt('fc_fabric_ssh_cert_path', + default='', + help='Local SSH certificate Path.'), cfg.StrOpt('zoning_policy', default='initiator-target', - help='overridden zoning policy'), + help='Overridden zoning policy.'), cfg.BoolOpt('zone_activate', default=True, - help='overridden zoning activation state'), + help='Overridden zoning activation state.'), cfg.StrOpt('zone_name_prefix', default='openstack', - help='overridden zone name prefix'), + help='Overridden zone name prefix.'), cfg.StrOpt('principal_switch_wwn', - help='Principal switch WWN of the fabric'), + default=None, + deprecated_for_removal=True, + help='Principal switch WWN of the fabric. This option is not ' + 'used anymore.') ] CONF = cfg.CONF @@ -59,5 +67,4 @@ def load_fabric_configurations(fabric_names): LOG.debug("Loaded FC fabric config %(fabricname)s", {'fabricname': fabric_name}) fabric_configs[fabric_name] = config - return fabric_configs diff --git a/cinder/zonemanager/drivers/brocade/brcd_fc_san_lookup_service.py b/cinder/zonemanager/drivers/brocade/brcd_fc_san_lookup_service.py index 05c24a422..e7ac96c35 100644 --- a/cinder/zonemanager/drivers/brocade/brcd_fc_san_lookup_service.py +++ b/cinder/zonemanager/drivers/brocade/brcd_fc_san_lookup_service.py @@ -28,6 +28,7 @@ from cinder import utils from cinder.zonemanager.drivers.brocade import brcd_fabric_opts as fabric_opts import cinder.zonemanager.drivers.brocade.fc_zone_constants as zone_constant from cinder.zonemanager import fc_san_lookup_service as fc_service +from cinder.zonemanager import utils as fczm_utils LOG = logging.getLogger(__name__) @@ -97,10 +98,10 @@ class BrcdFCSanLookupService(fc_service.FCSanLookupService): LOG.debug("FC Fabric List: %s", fabrics) if fabrics: for t in target_wwn_list: - formatted_target_list.append(self.get_formatted_wwn(t)) + formatted_target_list.append(fczm_utils.get_formatted_wwn(t)) for i in initiator_wwn_list: - formatted_initiator_list.append(self. + formatted_initiator_list.append(fczm_utils. get_formatted_wwn(i)) for fabric_name in fabrics: @@ -237,11 +238,3 @@ class BrcdFCSanLookupService(fc_service.FCSanLookupService): 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 index 1e6174e77..817e294d1 100644 --- a/cinder/zonemanager/drivers/brocade/brcd_fc_zone_client_cli.py +++ b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_client_cli.py @@ -1,8 +1,6 @@ # (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 @@ -34,7 +32,7 @@ from cinder import exception from cinder.i18n import _, _LE from cinder import ssh_utils from cinder import utils -import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant +import cinder.zonemanager.drivers.brocade.fc_zone_constants as zone_constant LOG = logging.getLogger(__name__) @@ -44,14 +42,17 @@ class BrcdFCZoneClientCLI(object): switch_port = '22' switch_user = 'admin' switch_pwd = 'none' + switch_key = 'none' patrn = re.compile('[;\s]+') - def __init__(self, ipaddress, username, password, port): - """initializing the client.""" + def __init__(self, ipaddress, username, + password, port, key): + """Initializing the client.""" self.switch_ip = ipaddress self.switch_port = port self.switch_user = username self.switch_pwd = password + self.switch_key = key self.sshpool = None def get_active_zone_set(self): @@ -77,7 +78,7 @@ class BrcdFCZoneClientCLI(object): zone_set_name = None try: switch_data = self._get_switch_info( - [ZoneConstant.GET_ACTIVE_ZONE_CFG]) + [zone_constant.GET_ACTIVE_ZONE_CFG]) except exception.BrocadeZoningCliException: with excutils.save_and_reraise_exception(): LOG.error(_LE("Failed getting active zone set " @@ -91,7 +92,7 @@ class BrcdFCZoneClientCLI(object): line_split = [x.replace( ' ', '') for x in line_split] - if ZoneConstant.CFG_ZONESET in line_split: + if zone_constant.CFG_ZONESET in line_split: zone_set_name = line_split[1] continue if line_split[1]: @@ -101,10 +102,10 @@ class BrcdFCZoneClientCLI(object): zone_member = line_split[2] 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 + zone_set[zone_constant.CFG_ZONES] = zone + zone_set[zone_constant.ACTIVE_ZONE_CONFIG] = zone_set_name except Exception: - # Incase of parsing error here, it should be malformed cli output. + # In case of parsing error here, it should be malformed cli output. msg = _("Malformed zone configuration: (switch=%(switch)s " "zone_config=%(zone_config)s)." ) % {'switch': self.switch_ip, @@ -137,7 +138,7 @@ class BrcdFCZoneClientCLI(object): if not active_zone_set: 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] + zone_list = active_zone_set[zone_constant.CFG_ZONES] LOG.debug("zone list: %s", zone_list) for zone in zones.keys(): # If zone exists, its an update. Delete & insert @@ -172,10 +173,10 @@ class BrcdFCZoneClientCLI(object): # Get active zone set from device, as some of the zones # could be deleted. active_zone_set = self.get_active_zone_set() - cfg_name = active_zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG] + cfg_name = active_zone_set[zone_constant.ACTIVE_ZONE_CONFIG] cmd = None if not cfg_name: - cfg_name = ZoneConstant.OPENSTACK_CFG_NAME + cfg_name = zone_constant.OPENSTACK_CFG_NAME cmd = 'cfgcreate "%(zoneset)s", "%(zones)s"' \ % {'zoneset': cfg_name, 'zones': zone_with_sep} else: @@ -197,21 +198,21 @@ class BrcdFCZoneClientCLI(object): def activate_zoneset(self, cfgname): """Method to Activate the zone config. Param cfgname - ZonesetName.""" - cmd_list = [ZoneConstant.ACTIVATE_ZONESET, cfgname] + cmd_list = [zone_constant.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) + return self._ssh_execute([zone_constant.DEACTIVATE_ZONESET], True, 1) def delete_zones(self, zone_names, activate, active_zone_set=None): """Delete zones from fabric. Method to delete the active zone config zones - params zone_names: zoneNames separated by semicolon - params activate: True/False - params active_zone_set: the active zone set dict retrieved + :param zone_names: zoneNames separated by semicolon + :param activate: True/False + :param active_zone_set: the active zone set dict retrieved from get_active_zone_set method """ active_zoneset_name = None @@ -219,8 +220,8 @@ class BrcdFCZoneClientCLI(object): if not active_zone_set: 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] + zone_constant.ACTIVE_ZONE_CONFIG] + zone_list = active_zone_set[zone_constant.CFG_ZONES] zones = self.patrn.split(''.join(zone_names)) cmd = None try: @@ -260,8 +261,8 @@ class BrcdFCZoneClientCLI(object): return_list = [] try: cmd = '%(nsshow)s;%(nscamshow)s' % { - 'nsshow': ZoneConstant.NS_SHOW, - 'nscamshow': ZoneConstant.NS_CAM_SHOW} + 'nsshow': zone_constant.NS_SHOW, + 'nscamshow': zone_constant.NS_CAM_SHOW} cli_output = self._get_switch_info([cmd]) except exception.BrocadeZoningCliException: with excutils.save_and_reraise_exception(): @@ -273,7 +274,7 @@ class BrcdFCZoneClientCLI(object): return return_list def _cfg_save(self): - self._ssh_execute([ZoneConstant.CFG_SAVE], True, 1) + self._ssh_execute([zone_constant.CFG_SAVE], True, 1) def _zone_delete(self, zone_name): cmd = 'zonedelete "%(zone_name)s"' % {'zone_name': zone_name} @@ -282,17 +283,17 @@ class BrcdFCZoneClientCLI(object): def _cfg_trans_abort(self): is_abortable = self._is_trans_abortable() if(is_abortable): - self.apply_zone_change([ZoneConstant.CFG_ZONE_TRANS_ABORT]) + self.apply_zone_change([zone_constant.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) + [zone_constant.CFG_SHOW_TRANS], True, 1) output = stdout.splitlines() is_abortable = False for line in output: - if(ZoneConstant.TRANS_ABORTABLE in line): + if(zone_constant.TRANS_ABORTABLE in line): is_abortable = True break if stderr: @@ -392,6 +393,7 @@ class BrcdFCZoneClientCLI(object): None, self.switch_user, self.switch_pwd, + self.switch_key, min_size=1, max_size=5) last_exception = None @@ -438,6 +440,7 @@ class BrcdFCZoneClientCLI(object): None, self.switch_user, self.switch_pwd, + self.switch_key, min_size=1, max_size=5) stdin, stdout, stderr = None, None, None @@ -449,7 +452,7 @@ class BrcdFCZoneClientCLI(object): attempts -= 1 try: stdin, stdout, stderr = ssh.exec_command(command) - stdin.write("%s\n" % ZoneConstant.YES) + stdin.write("%s\n" % zone_constant.YES) channel = stdout.channel exit_status = channel.recv_exit_status() LOG.debug("Exit Status from ssh: %s", exit_status) @@ -499,7 +502,7 @@ class BrcdFCZoneClientCLI(object): 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. + Invokes 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. """ @@ -512,6 +515,7 @@ class BrcdFCZoneClientCLI(object): None, self.switch_user, self.switch_pwd, + self.switch_key, min_size=1, max_size=5) with self.sshpool.item() as ssh: @@ -545,6 +549,7 @@ exit channel.close() except Exception: LOG.exception(_LE('Error closing channel.')) + LOG.debug("_execute_cmd: stdout to return: %s", stdout) LOG.debug("_execute_cmd: stderr to return: %s", stderr) return (stdout, stderr) diff --git a/cinder/zonemanager/drivers/brocade/brcd_fc_zone_connector_factory.py b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_connector_factory.py new file mode 100644 index 000000000..0a4234d89 --- /dev/null +++ b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_connector_factory.py @@ -0,0 +1,82 @@ +# (c) Copyright 2015 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +""" +Brocade Zone Connector Factory is responsible to dynamically create the +connection object based on the configuration +""" + +from oslo_log import log as logging +from oslo_utils import importutils + +from cinder.zonemanager.drivers.brocade import fc_zone_constants + +LOG = logging.getLogger(__name__) + + +class BrcdFCZoneFactory(object): + + def __init__(self): + self.sb_conn_map = {} + + def get_connector(self, fabric, sb_connector): + """Returns Device Connector. + + Factory method to create and return + correct SB connector object based on the protocol + """ + + fabric_ip = fabric.safe_get('fc_fabric_address') + client = self.sb_conn_map.get(fabric_ip) + + if not client: + + fabric_user = fabric.safe_get('fc_fabric_user') + fabric_pwd = fabric.safe_get('fc_fabric_password') + fabric_port = fabric.safe_get('fc_fabric_port') + fabric_ssh_cert_path = fabric.safe_get('fc_fabric_ssh_cert_path') + + LOG.debug("Client not found. Creating connection client for" + " %(ip)s with %(connector)s protocol " + "for the user %(user)s at port %(port)s.", + {'ip': fabric_ip, + 'connector': sb_connector, + 'user': fabric_user, + 'port': fabric_port}) + + if sb_connector.lower() in (fc_zone_constants.HTTP, + fc_zone_constants.HTTPS): + client = importutils.import_object( + "cinder.zonemanager.drivers.brocade." + "brcd_http_fc_zone_client.BrcdHTTPFCZoneClient", + ipaddress=fabric_ip, + username=fabric_user, + password=fabric_pwd, + port=fabric_port, + protocol=sb_connector + ) + else: + client = importutils.import_object( + "cinder.zonemanager.drivers.brocade." + "brcd_fc_zone_client_cli.BrcdFCZoneClientCLI", + ipaddress=fabric_ip, + username=fabric_user, + password=fabric_pwd, + key=fabric_ssh_cert_path, + port=fabric_port + ) + self.sb_conn_map.update({fabric_ip: client}) + return client diff --git a/cinder/zonemanager/drivers/brocade/brcd_fc_zone_driver.py b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_driver.py index 93b143855..21cfbe962 100644 --- a/cinder/zonemanager/drivers/brocade/brcd_fc_zone_driver.py +++ b/cinder/zonemanager/drivers/brocade/brcd_fc_zone_driver.py @@ -1,8 +1,6 @@ # (c) Copyright 2015 Brocade Communications Systems Inc. # All Rights Reserved. # -# Copyright 2015 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 @@ -41,17 +39,18 @@ import string from cinder import exception from cinder.i18n import _, _LE, _LI, _LW from cinder.zonemanager.drivers.brocade import brcd_fabric_opts as fabric_opts +from cinder.zonemanager.drivers.brocade import fc_zone_constants from cinder.zonemanager.drivers import driver_utils from cinder.zonemanager.drivers import fc_zone_driver +from cinder.zonemanager import utils LOG = logging.getLogger(__name__) SUPPORTED_CHARS = string.ascii_letters + string.digits + '_' brcd_opts = [ cfg.StrOpt('brcd_sb_connector', - default='cinder.zonemanager.drivers.brocade' - '.brcd_fc_zone_client_cli.BrcdFCZoneClientCLI', - help='Southbound connector for zoning operation'), + default=fc_zone_constants.HTTP.upper(), + help='South bound connector for zoning operation'), ] CONF = cfg.CONF @@ -68,9 +67,10 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): 1.0 - Initial Brocade FC zone driver 1.1 - Implements performance enhancements 1.2 - Added support for friendly zone name + 1.3 - Added HTTP connector support """ - VERSION = "1.2" + VERSION = "1.3" def __init__(self, **kwargs): super(BrcdFCZoneDriver, self).__init__(**kwargs) @@ -103,15 +103,6 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): self.fabric_configs = fabric_opts.load_fabric_configurations( fabric_names) - 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 b':'.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, host_name=None, storage_system=None): @@ -147,8 +138,8 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): "no zoning will be performed.")) return - cli_client = self._get_cli_client(fabric) - cfgmap_from_fabric = self._get_active_zone_set(cli_client) + client = self._get_southbound_client(fabric) + cfgmap_from_fabric = self._get_active_zone_set(client) zone_names = [] if cfgmap_from_fabric.get('zones'): @@ -158,12 +149,11 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): for initiator_key in initiator_target_map.keys(): zone_map = {} initiator = initiator_key.lower() - t_list = initiator_target_map[initiator_key] + target_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)] + for target in target_list: + zone_members = [utils.get_formatted_wwn(initiator), + utils.get_formatted_wwn(target)] zone_name = driver_utils.get_friendly_zone_name( zoning_policy, initiator, @@ -172,8 +162,7 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): storage_system, zone_name_prefix, SUPPORTED_CHARS) - if ( - len(cfgmap_from_fabric) == 0 or ( + if (len(cfgmap_from_fabric) == 0 or ( zone_name not in zone_names)): zone_map[zone_name] = zone_members else: @@ -182,10 +171,9 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): "zone creation for %(zonename)s"), {'zonename': 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_members = [utils.get_formatted_wwn(initiator)] + for target in target_list: + zone_members.append(utils.get_formatted_wwn(target)) zone_name = driver_utils.get_friendly_zone_name( zoning_policy, @@ -208,11 +196,12 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): if len(zone_map) > 0: try: - cli_client.add_zones( + client.add_zones( zone_map, zone_activate, cfgmap_from_fabric) - cli_client.cleanup() - except exception.BrocadeZoningCliException as brocade_ex: + client.cleanup() + except (exception.BrocadeZoningCliException, + exception.BrocadeZoningHttpException) as brocade_ex: raise exception.FCZoneDriverException(brocade_ex) except Exception: msg = _("Failed to add zoning configuration.") @@ -248,7 +237,7 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): zoning_policy = zoning_policy_fab LOG.info(_LI("Zoning policy for fabric %(policy)s"), {'policy': zoning_policy}) - conn = self._get_cli_client(fabric) + conn = self._get_southbound_client(fabric) cfgmap_from_fabric = self._get_active_zone_set(conn) zone_names = [] @@ -262,7 +251,7 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): {'cfgmap': cfgmap_from_fabric}) for initiator_key in initiator_target_map.keys(): initiator = initiator_key.lower() - formatted_initiator = self.get_formatted_wwn(initiator) + formatted_initiator = utils.get_formatted_wwn(initiator) zone_map = {} zones_to_delete = [] t_list = initiator_target_map[initiator_key] @@ -290,7 +279,7 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): zone_members = [formatted_initiator] for t in t_list: target = t.lower() - zone_members.append(self.get_formatted_wwn(target)) + zone_members.append(utils.get_formatted_wwn(target)) zone_name = driver_utils.get_friendly_zone_name( zoning_policy, @@ -353,8 +342,12 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): zone_name_string, zone_activate, cfgmap_from_fabric) conn.cleanup() + except (exception.BrocadeZoningCliException, + exception.BrocadeZoningHttpException) as brocade_ex: + raise exception.FCZoneDriverException(brocade_ex) except Exception: - msg = _("Failed to update or delete zoning configuration") + msg = _("Failed to update or delete zoning " + "configuration.") LOG.exception(msg) raise exception.FCZoneDriverException(msg) @@ -364,7 +357,6 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): 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 = {} fc_fabric_names = self.configuration.fc_fabric_names @@ -374,11 +366,11 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): {'targetwwns': target_wwn_list}) if len(fabrics) > 0: for t in target_wwn_list: - formatted_target_list.append(self.get_formatted_wwn(t.lower())) + formatted_target_list.append(utils.get_formatted_wwn(t)) LOG.debug("Formatted target WWN list: %(targetlist)s", {'targetlist': formatted_target_list}) for fabric_name in fabrics: - conn = self._get_cli_client(fabric_name) + conn = self._get_southbound_client(fabric_name) # Get name server data from fabric and get the targets # logged in. @@ -388,12 +380,13 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): LOG.debug("Name server info from fabric: %(nsinfo)s", {'nsinfo': nsinfo}) conn.cleanup() - except exception.BrocadeZoningCliException: + except (exception.BrocadeZoningCliException, + exception.BrocadeZoningHttpException): if not conn.is_supported_firmware(): msg = _("Unsupported firmware on switch %s. Make sure " "switch is running firmware v6.4 or higher" ) % conn.switch_ip - LOG.error(msg) + LOG.exception(msg) raise exception.FCZoneDriverException(msg) with excutils.save_and_reraise_exception(): LOG.exception(_LE("Error getting name server info.")) @@ -425,41 +418,47 @@ class BrcdFCZoneDriver(fc_zone_driver.FCZoneDriver): cfgmap = None try: cfgmap = conn.get_active_zone_set() - except exception.BrocadeZoningCliException: + except (exception.BrocadeZoningCliException, + exception.BrocadeZoningHttpException): if not conn.is_supported_firmware(): msg = _("Unsupported firmware on switch %s. Make sure " "switch is running firmware v6.4 or higher" ) % conn.switch_ip LOG.error(msg) raise exception.FCZoneDriverException(msg) + with excutils.save_and_reraise_exception(): + LOG.exception(_LE("Error getting name server info.")) except Exception as e: msg = (_("Failed to retrieve active zoning configuration %s") % six.text_type(e)) + LOG.error(msg) raise exception.FCZoneDriverException(msg) LOG.debug("Active zone set from fabric: %(cfgmap)s", {'cfgmap': cfgmap}) return cfgmap - def _get_cli_client(self, fabric): - fabric_ip = self.fabric_configs[fabric].safe_get('fc_fabric_address') - fabric_user = self.fabric_configs[fabric].safe_get('fc_fabric_user') - fabric_pwd = self.fabric_configs[fabric].safe_get('fc_fabric_password') - fabric_port = self.fabric_configs[fabric].safe_get('fc_fabric_port') - cli_client = None + def _get_southbound_client(self, fabric): + """Implementation to get SouthBound Connector. + + South bound connector will be + dynamically selected based on the configuration + + :param fabric: fabric information + """ + fabric_info = self.fabric_configs[fabric] + fc_ip = fabric_info.safe_get('fc_fabric_address') + sb_connector = fabric_info.safe_get('fc_southbound_protocol') + if sb_connector is None: + sb_connector = self.configuration.brcd_sb_connector try: - cli_client = self.sb_conn_map.get(fabric_ip) - if not cli_client: - LOG.debug("CLI client not found, creating for %(ip)s", - {'ip': fabric_ip}) - cli_client = importutils.import_object( - self.configuration.brcd_sb_connector, - ipaddress=fabric_ip, - username=fabric_user, - password=fabric_pwd, - port=fabric_port) - self.sb_conn_map[fabric_ip] = cli_client - except Exception as e: - LOG.error(e) - msg = _("Failed to create sb connector for %s") % fabric_ip + conn_factory = importutils.import_object( + "cinder.zonemanager.drivers.brocade." + "brcd_fc_zone_connector_factory." + "BrcdFCZoneFactory") + client = conn_factory.get_connector(fabric_info, + sb_connector.upper()) + except Exception: + msg = _("Failed to create south bound connector for %s.") % fc_ip + LOG.exception(msg) raise exception.FCZoneDriverException(msg) - return cli_client + return client diff --git a/cinder/zonemanager/drivers/brocade/brcd_http_fc_zone_client.py b/cinder/zonemanager/drivers/brocade/brcd_http_fc_zone_client.py new file mode 100644 index 000000000..7c4398c04 --- /dev/null +++ b/cinder/zonemanager/drivers/brocade/brcd_http_fc_zone_client.py @@ -0,0 +1,734 @@ +# (c) Copyright 2015 Brocade Communications Systems Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +""" +Brocade south bound connector to communicate with switch using +HTTP or HTTPS protocol. +""" + +from oslo_log import log as logging +import requests +import six +import time + +from cinder import exception +from cinder.i18n import _ +import cinder.zonemanager.drivers.brocade.fc_zone_constants as zone_constant + + +LOG = logging.getLogger(__name__) + + +class BrcdHTTPFCZoneClient(object): + + def __init__(self, ipaddress, username, + password, port, protocol): + """Initializing the client with the parameters passed. + + Creates authentication token and authenticate with switch + to ensure the credentials are correct. + + :param ipaddress: IP Address of the device. + :param username: User id to login. + :param password: User password. + :param port: Device Communication port + :param protocol: Communication Protocol. + """ + self.switch_ip = ipaddress + self.switch_user = username + self.switch_pwd = password + self.protocol = protocol + self.cfgs = {} + self.zones = {} + self.alias = {} + self.qlps = {} + self.ifas = {} + self.active_cfg = '' + self.parsed_raw_zoneinfo = "" + self.random_no = '' + self.session = None + + # Create and assign the authentication header based on the credentials + self.auth_header = self.create_auth_token() + + # Authenticate with the switch + # If authenticated successfully, save the auth status and + # create auth header for future communication with the device. + self.is_auth, self.auth_header = self.authenticate() + + def connect(self, requestType, requestURL, payload='', header=None): + """Connect to the switch using HTTP/HTTPS protocol. + + :param requestType: Connection Request method + :param requestURL: Connection URL + :param payload: Data to send with POST request + :param header: Request Headers + + :returns: HTTP response data + :raises: BrocadeZoningHttpException + """ + try: + if header is None: + header = {} + header.update({"User-Agent": "OpenStack Zone Driver"}) + + # Ensure only one connection is made throughout the life cycle + protocol = zone_constant.HTTP + if self.protocol == zone_constant.PROTOCOL_HTTPS: + protocol = zone_constant.HTTPS + if self.session is None: + self.session = requests.Session() + adapter = requests.adapters.HTTPAdapter(pool_connections=1, + pool_maxsize=1) + self.session.mount(protocol + '://', adapter) + url = protocol + "://" + self.switch_ip + requestURL + response = None + if requestType == zone_constant.GET_METHOD: + response = self.session.get(url, + headers=(header), + verify=False) + elif requestType == zone_constant.POST_METHOD: + response = self.session.post(url, + payload, + headers=(header), + verify=False) + + # Throw exception when response status is not OK + if response.status_code != zone_constant.STATUS_OK: + msg = _("Error while querying page %(url)s on the switch, " + "reason %(error)s.") % {'url': url, + 'error': response.reason} + raise exception.BrocadeZoningHttpException(msg) + else: + return response.text + except requests.exceptions.ConnectionError as e: + msg = (_("Error while connecting the switch %(switch_id)s " + "with protocol %(protocol)s. Error: %(error)s.") + % {'switch_id': self.switch_ip, + 'protocol': self.protocol, + 'error': six.text_type(e)}) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + except exception.BrocadeZoningHttpException as ex: + msg = (_("Unexpected status code from the switch %(switch_id)s " + "with protocol %(protocol)s for url %(page)s. " + "Error: %(error)s") + % {'switch_id': self.switch_ip, + 'protocol': self.protocol, + 'page': requestURL, + 'error': six.text_type(ex)}) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + + def create_auth_token(self): + """Create the authentication token. + + Creates the authentication token to use in the authentication header + return authentication header (Base64(username:password:random no)). + + :returns: Authentication Header + :raises: BrocadeZoningHttpException + """ + try: + # Send GET request to secinfo.html to get random number + response = self.connect(zone_constant.GET_METHOD, + zone_constant.SECINFO_PAGE) + parsed_data = self.get_parsed_data(response, + zone_constant.SECINFO_BEGIN, + zone_constant.SECINFO_END) + + # Extract the random no from secinfo.html response + self.random_no = self.get_nvp_value(parsed_data, + zone_constant.RANDOM) + # Form the authentication string + auth_string = (self.switch_user + ":" + self.switch_pwd + + ":" + self.random_no) + auth_token = auth_string.encode( + "base64", "strict").strip() # encode in base64 format + auth_header = (zone_constant.AUTH_STRING + + auth_token) # Build the proper header + except Exception as e: + msg = (_("Error while creating authentication token: %s") + % six.text_type(e)) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + return auth_header + + def authenticate(self): + """Authenticate with the switch. + + Returns authentication status with modified authentication + header (Base64(username:xxx:random no)). + + :returns: Authentication status + :raises: BrocadeZoningHttpException + """ + headers = {zone_constant.AUTH_HEADER: self.auth_header} + try: + # GET Request to authenticate.html to verify the credentials + response = self.connect(zone_constant.GET_METHOD, + zone_constant.AUTHEN_PAGE, + header=headers) + parsed_data = self.get_parsed_data(response, + zone_constant.AUTHEN_BEGIN, + zone_constant.AUTHEN_END) + isauthenticated = self.get_nvp_value( + parsed_data, zone_constant.AUTHENTICATED) + if isauthenticated == "yes": + # Replace password in the authentication string with xxx + auth_string = (self.switch_user + + ":" + "xxx" + ":" + self.random_no) + auth_token = auth_string.encode("base64", "strict").strip() + auth_header = zone_constant.AUTH_STRING + auth_token + return True, auth_header + else: + auth_error_code = self.get_nvp_value(parsed_data, "errCode") + msg = (_("Authentication failed, verify the switch " + "credentials, error code %s.") % auth_error_code) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + except Exception as e: + msg = (_("Error while authenticating with switch: %s.") + % six.text_type(e)) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + + def get_session_info(self): + """Get the session information from the switch + + :returns: Connection status information. + """ + try: + headers = {zone_constant.AUTH_HEADER: self.auth_header} + # GET request to session.html + response = self.connect(zone_constant.GET_METHOD, + zone_constant.SESSION_PAGE_ACTION, + header=headers) + except Exception as e: + msg = (_("Error while getting session information %s.") + % six.text_type(e)) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + return response + + def get_parsed_data(self, data, delim1, demil2): + """Return the sub string between the delimiters. + + :param data: String to manipulate + :param delim1 : Delimiter 1 + :param delim2 : Delimiter 2 + :returns: substring between the delimiters + """ + try: + start = data.index(delim1) + start = start + len(delim1) + end = data.index(demil2) + return data[start:end] + except ValueError as e: + msg = (_("Error while parsing the data: %s.") % six.text_type(e)) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + + def get_nvp_value(self, data, keyname): + """Get the value for the key passed. + + :param data: NVP to manipulate + :param keyname: Key name + :returns: value for the NVP + """ + try: + start = data.index(keyname) + start = start + len(keyname) + temp = data[start:] + end = temp.index("\n") + return (temp[:end].lstrip('= ')) + except ValueError as e: + msg = (_("Error while getting nvp value: %s.") % six.text_type(e)) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + + def get_zone_info(self): + """Parse all the zone information and store it in the dictionary.""" + + try: + self.cfgs = {} + self.zones = {} + self.active_cfg = '' + self.alias = {} + self.qlps = {} + self.ifas = {} + headers = {zone_constant.AUTH_HEADER: self.auth_header} + # GET request to gzoneinfo.htm + response = self.connect(zone_constant.GET_METHOD, + zone_constant.ZONE_PAGE, + header=headers) + # get the zone string from the response + self.parsed_raw_zoneinfo = self.get_parsed_data( + response, + zone_constant.ZONEINFO_BEGIN, + zone_constant.ZONEINFO_END).strip("\n") + LOG.debug("Original zone string from the switch: %(zoneinfo)s", + {'zoneinfo': self.parsed_raw_zoneinfo}) + # convert the zone string to list + zoneinfo = self.parsed_raw_zoneinfo.split() + i = 0 + while i < len(zoneinfo): + info = zoneinfo[i] + # check for the cfg delimiter + if zone_constant.CFG_DELIM in info: + # extract the cfg name + cfg_name = info.lstrip(zone_constant.CFG_DELIM) + # update the dict as + # self.cfgs={cfg_name:zone_name1;zone_name2} + self.cfgs.update({cfg_name: zoneinfo[i + 1]}) + i = i + 2 + # check for the zone delimiter + elif zone_constant.ZONE_DELIM in info: + # extract the zone name + zone_name = info.lstrip(zone_constant.ZONE_DELIM) + # update the dict as + # self.zones={zone_name:members1;members2} + self.zones.update({zone_name: zoneinfo[i + 1]}) + i = i + 2 + elif zone_constant.ALIAS_DELIM in info: + alias_name = info.lstrip(zone_constant.ALIAS_DELIM) + # update the dict as + # self.alias={alias_name:members1;members2} + self.alias.update({alias_name: zoneinfo[i + 1]}) + i = i + 2 + # check for quickloop zones + elif zone_constant.QLP_DELIM in info: + qlp_name = info.lstrip(zone_constant.QLP_DELIM) + # update the map as self.qlps={qlp_name:members1;members2} + self.qlps.update({qlp_name: zoneinfo[i + 1]}) + i = i + 2 + # check for fabric assist zones + elif zone_constant.IFA_DELIM in info: + ifa_name = info.lstrip(zone_constant.IFA_DELIM) + # update the map as self.ifas={ifa_name:members1;members2} + self.ifas.update({ifa_name: zoneinfo[i + 1]}) + i = i + 2 + elif zone_constant.ACTIVE_CFG_DELIM in info: + # update the string self.active_cfg=cfg_name + self.active_cfg = info.lstrip( + zone_constant.ACTIVE_CFG_DELIM) + if self.active_cfg == zone_constant.DEFAULT_CFG: + self.active_cfg = "" + i = i + 2 + else: + i = i + 1 + except Exception as e: + msg = (_("Error while changing VF context %s.") % six.text_type(e)) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(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. + + :returns: True if firmware is supported else False. + :raises: BrocadeZoningHttpException + """ + + isfwsupported = False + + try: + headers = {zone_constant.AUTH_HEADER: self.auth_header} + # GET request to switch.html + response = self.connect(zone_constant.GET_METHOD, + zone_constant.SWITCH_PAGE, + header=headers) + parsed_data = self.get_parsed_data(response, + zone_constant.SWITCHINFO_BEGIN, + zone_constant.SWITCHINFO_END) + + # get the firmware version nvp value + fwVersion = self.get_nvp_value( + parsed_data, + zone_constant.FIRMWARE_VERSION).lstrip('v') + + ver = fwVersion.split(".") + LOG.debug("Firmware version: %(version)s.", {'version': ver}) + if int(ver[0] + ver[1]) > 63: + isfwsupported = True + + except Exception as e: + msg = (_("Error while checking the firmware version %s.") + % six.text_type(e)) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + return isfwsupported + + 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' + } + :raises: BrocadeZoningHttpException + """ + active_zone_set = {} + zones_map = {} + try: + self.get_zone_info() # get the zone information of the switch + if self.active_cfg != '': + # get the zones list of the active_Cfg + zones_list = self.cfgs[self.active_cfg].split(";") + for n in zones_list: + # build the zones map + zones_map.update( + {n: self.zones[n].split(";")}) + # Format map in the correct format + active_zone_set = { + "active_zone_config": self.active_cfg, "zones": zones_map} + return active_zone_set + except Exception as e: + msg = (_("Failed getting active zone set from fabric %s.") + % six.text_type(e)) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + + def add_zones(self, add_zones_info, activate, active_zone_set=None): + """Add zone configuration. + + This method will add the zone configuration passed by user. + + :param add_zones_info: 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'] + }R + :param activate: True will activate the zone config. + :param active_zone_set: Active zone set dict retrieved from + get_active_zone_set method + :raises: BrocadeZoningHttpException + """ + LOG.debug("Add zones - zones passed: %(zones)s.", + {'zones': add_zones_info}) + cfg_name = zone_constant.CFG_NAME + cfgs = self.cfgs + zones = self.zones + alias = self.alias + qlps = self.qlps + ifas = self.ifas + active_cfg = self.active_cfg + # update the active_cfg, zones and cfgs map with new information + zones, cfgs, active_cfg = self.add_update_zones_cfgs(cfgs, + zones, + add_zones_info, + active_cfg, + cfg_name) + # Build the zonestring with updated maps + data = self.form_zone_string(cfgs, + active_cfg, + zones, + alias, + qlps, + ifas, + activate) + LOG.debug("Add zones: final zone string after applying " + "to the switch: %(zonestring)s", {'zonestring': data}) + # Post the zone data to the switch + error_code, error_msg = self.post_zone_data(data) + if error_code != "0": + msg = (_("Applying the zones and cfgs to the switch failed " + "(error code=%(err_code)s error msg=%(err_msg)s.") + % {'err_code': error_code, 'err_msg': error_msg}) + + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + + def form_zone_string(self, cfgs, active_cfg, + zones, alias, qlps, ifas, activate): + """Build the zone string in the required format. + + :param cfgs: cfgs map + :param active_cfg: Active cfg string + :param zones: zones map + :param alias: alias map + :param qlps: qlps map + :param ifas: ifas map + :param activate: True will activate config. + :returns: zonestring in the required format + :raises: BrocadeZoningHttpException + """ + try: + zoneString = zone_constant.ZONE_STRING_PREFIX + + # based on the activate save only will be changed + saveonly = "false" if activate is True else "true" + + # Form the zone string based on the dictionary of each items + for cfg in cfgs.keys(): + zoneString += (zone_constant.CFG_DELIM + + cfg + " " + cfgs.get(cfg) + " ") + for zone in zones.keys(): + zoneString += (zone_constant.ZONE_DELIM + + zone + " " + zones.get(zone) + " ") + for al in alias.keys(): + zoneString += (zone_constant.ALIAS_DELIM + + al + " " + alias.get(al) + " ") + for qlp in qlps.keys(): + zoneString += (zone_constant.QLP_DELIM + + qlp + " " + qlps.get(qlp) + " ") + for ifa in ifas.keys(): + zoneString += (zone_constant.IFA_DELIM + + ifa + " " + ifas.get(ifa) + " ") + # append the active_cfg string only if it is not null and activate + # is true + if active_cfg != "" and activate: + zoneString += (zone_constant.ACTIVE_CFG_DELIM + + active_cfg + " null ") + # Build the final zone string + zoneString += zone_constant.ZONE_END_DELIM + saveonly + except Exception as e: + msg = (_("Exception while forming the zone string: %s.") + % six.text_type(e)) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + return zoneString + + def add_update_zones_cfgs(self, cfgs, zones, add_zones_info, + active_cfg, cfg_name): + """Add or update the zones and cfgs map based on the new zones info. + + This method will return the updated zones,cfgs and active_cfg + + :param cfgs: Existing cfgs map + :param active_cfg: Existing Active cfg string + :param zones: Existing zones map + :param add_zones_info :Zones map to add + :param active_cfg :Existing active cfg + :param cfg_name : New cfg name + :returns: updated zones, zone configs map, and active_cfg + """ + cfg_string = "" + delimiter = "" + zones_in_active_cfg = "" + try: + if active_cfg: + zones_in_active_cfg = cfgs.get(active_cfg) + for zone_name, members in add_zones_info.items(): + # if new zone is not active_cfg, build the cfg string with the + # new zones + if zone_name not in zones_in_active_cfg: + cfg_string += delimiter + zone_name + delimiter = ";" + # update the zone string + # if zone name already exists and dont have the new members + # already + if (zone_name in zones and set(members) + != set(zones.get(zone_name).split(";"))): + # update the existing zone with new members + zones.update( + {zone_name: (";".join(members) + + ";" + zones.get(zone_name))}) + else: + # add a new zone with the members + zones.update({zone_name: ";".join(members)}) + # update cfg string + if active_cfg: + if cfg_string: + # update the existing active cfg map with cfgs string + cfgs.update( + {active_cfg: cfg_string + ";" + cfgs.get(active_cfg)}) + else: + # create new cfg and update that cfgs map with the new cfg + active_cfg = cfg_name + cfgs.update({cfg_name: cfg_string}) + except Exception as e: + msg = (_("Error while updating the new zones and cfgs " + "in the zone string. Error %(description)s.") + % {'description': six.text_type(e)}) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + return zones, cfgs, active_cfg + + def get_nameserver_info(self): + """Get name server data from fabric. + + Return the connected node port wwn list(local + and remote) for the given switch fabric. + + :returns: name server information. + """ + nsinfo = [] + headers = {zone_constant.AUTH_HEADER: self.auth_header} + response = self.connect(zone_constant.GET_METHOD, + zone_constant.NS_PAGE, + header=headers) # GET request to nsinfo.html + parsed_raw_zoneinfo = self.get_parsed_data( + response, + zone_constant.NSINFO_BEGIN, + zone_constant.NSINFO_END).strip("\t\n\r") + # build the name server information in the correct format + for line in parsed_raw_zoneinfo.splitlines(): + start_index = line.find(zone_constant.NS_DELIM) + 7 + if start_index != -1: + nsinfo.extend([line[start_index:start_index + 23].strip()]) + return nsinfo + + def delete_update_zones_cfgs( + self, cfgs, zones, + delete_zones_info, active_cfg): + """Add or update the zones and cfgs map based on the new zones info. + + Return the updated zones, cfgs and active_cfg after deleting the + required items. + + :param cfgs: Existing cfgs map + :param active_cfg: Existing Active cfg string + :param zones: Existing zones map + :param delete_zones_info :Zones map to add + :param active_cfg :Existing active cfg + :returns: updated zones, zone config sets, and active zone config + :raises: BrocadeZoningHttpException + """ + try: + delete_zones_info = delete_zones_info.split(";") + for zone in delete_zones_info: + # remove the zones from the zone map + zones.pop(zone) + # iterated all the cfgs, but need to check since in SSH only + # active cfg is iterated + for k, v in cfgs.items(): + v = v.split(";") + if zone in v: + # remove the zone from the cfg string + v.remove(zone) + # if all the zones are removed, remove the cfg from the + # cfg map + if not v: + cfgs.pop(k) + # update the original cfg with the updated string + else: + cfgs[k] = ";".join(v) + + # if all the zones are removed in the active_cfg, update it with + # empty string + if active_cfg not in cfgs: + active_cfg = "" + except KeyError as e: + msg = (_("Error while removing the zones and cfgs " + "in the zone string: %(description)s.") + % {'description': six.text_type(e)}) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + return zones, cfgs, active_cfg + + def delete_zones(self, delete_zones_info, activate, active_zone_set=None): + """Delete zones from fabric. + + Deletes zones in the active zone config. + + :param zone_names: zoneNames separated by semicolon + :param activate: True/False + :param active_zone_set: the active zone set dict retrieved + from get_active_zone_set method + """ + cfgs = self.cfgs + zones = self.zones + alias = self.alias + qlps = self.qlps + ifas = self.ifas + active_cfg = self.active_cfg + # update the active_cfg, zones and cfgs map with required information + # being removed + zones, cfgs, active_cfg = self.delete_update_zones_cfgs( + cfgs, + zones, + delete_zones_info, + active_cfg) + # Build the zonestring with updated maps + data = self.form_zone_string(cfgs, + active_cfg, + zones, + alias, + qlps, + ifas, + activate) + LOG.debug("Delete zones: final zone string after applying " + "to the switch: %(zonestring)s", {'zonestring': data}) + error_code, error_msg = self.post_zone_data(data) + if error_code != "0": + msg = (_("Applying the zones and cfgs to the switch failed " + "(error code=%(err_code)s error msg=%(err_msg)s.") + % {'err_code': error_code, 'err_msg': error_msg}) + LOG.error(msg) + raise exception.BrocadeZoningHttpException(reason=msg) + + def post_zone_data(self, data): + """Send POST request to the switch with the payload. + + :param data: payload to be sent to switch + """ + + status = "progress" + parsed_data_txn = "" + headers = {zone_constant.AUTH_HEADER: self.auth_header} + + LOG.debug("Requesting the switch with posting the zone string.") + # POST request to gzoneinfo with zonestring as payload + response = self.connect(zone_constant.POST_METHOD, + zone_constant.ZONE_PAGE, + data, + headers) + parsed_data = self.get_parsed_data(response, + zone_constant.ZONE_TX_BEGIN, + zone_constant.ZONE_TX_END) + transID = self.get_nvp_value(parsed_data, + zone_constant.ZONE_TX_ID) + transURL = zone_constant.ZONE_TRAN_STATUS.format(txnId=transID) + timeout = 360 + sleep_time = 3 + time_elapsed = 0 + while(status != "done"): + txn_response = self.connect( + zone_constant.GET_METHOD, transURL, "", headers) + parsed_data_txn = self.get_parsed_data(txn_response, + zone_constant.ZONE_TX_BEGIN, + zone_constant.ZONE_TX_END) + status = self.get_nvp_value(parsed_data_txn, + zone_constant.ZONE_TX_STATUS) + time.sleep(sleep_time) + time_elapsed += sleep_time + if time_elapsed > timeout: + break + if status != "done": + errorCode = -1 + errorMessage = ("Timed out, waiting for zone transaction on " + "the switch to complete") + else: + errorCode = self.get_nvp_value(parsed_data_txn, + zone_constant.ZONE_ERROR_CODE) + errorMessage = self.get_nvp_value(parsed_data_txn, + zone_constant.ZONE_ERROR_MSG) + return errorCode, errorMessage + + def cleanup(self): + """Close session.""" + self.session.close() diff --git a/cinder/zonemanager/drivers/brocade/fc_zone_constants.py b/cinder/zonemanager/drivers/brocade/fc_zone_constants.py index 1cc2372e5..e9aa241fd 100644 --- a/cinder/zonemanager/drivers/brocade/fc_zone_constants.py +++ b/cinder/zonemanager/drivers/brocade/fc_zone_constants.py @@ -1,8 +1,6 @@ # (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 @@ -44,3 +42,60 @@ CFG_SHOW_TRANS = 'cfgtransshow' CFG_ZONE_TRANS_ABORT = 'cfgtransabort' NS_SHOW = 'nsshow' NS_CAM_SHOW = 'nscamshow' + +""" +HTTPS connector constants +""" +AUTH_HEADER = "Authorization" +PROTOCOL_HTTPS = "HTTPS" +STATUS_OK = 200 +SECINFO_PAGE = "/secinfo.html" +AUTHEN_PAGE = "/authenticate.html" +GET_METHOD = "GET" +POST_METHOD = "POST" +SECINFO_BEGIN = "--BEGIN SECINFO" +SECINFO_END = "--END SECINFO" +RANDOM = "RANDOM" +AUTH_STRING = "Custom_Basic " # Trailing space is required, do not remove +AUTHEN_BEGIN = "--BEGIN AUTHENTICATE" +AUTHEN_END = "--END AUTHENTICATE" +AUTHENTICATED = "authenticated" +SESSION_PAGE_ACTION = "/session.html?action=query" +SESSION_BEGIN = "--BEGIN SESSION" +SESSION_END = "--END SESSION" +SESSION_PAGE = "/session.html" +ZONEINFO_BEGIN = "--BEGIN ZONE INFO" +ZONEINFO_END = "--END ZONE INFO" +SWITCH_PAGE = "/switch.html" +SWITCHINFO_BEGIN = "--BEGIN SWITCH INFORMATION" +SWITCHINFO_END = "--END SWITCH INFORMATION" +FIRMWARE_VERSION = "swFWVersion" +VF_ENABLED = "vfEnabled" +MANAGEABLE_VF = "manageableLFList" +CHANGE_VF = ("Session=--BEGIN SESSION\n\taction=apply\n\tLFId= {vfid} " + "\b\t--END SESSION") +ZONE_TRAN_STATUS = "/gzoneinfo.htm?txnId={txnId}" +CFG_DELIM = "\x01" +ZONE_DELIM = "\x02" +ALIAS_DELIM = "\x03" +QLP_DELIM = "\x04" +ZONE_END_DELIM = "\x05&saveonly=" +IFA_DELIM = "\x06" +ACTIVE_CFG_DELIM = "\x07" +DEFAULT_CFG = "d__efault__Cfg" +NS_PAGE = "/nsinfo.htm" +NSINFO_BEGIN = "--BEGIN NS INFO" +NSINFO_END = "--END NS INFO" +NS_DELIM = ";N ;" +ZONE_TX_BEGIN = "--BEGIN ZONE_TXN_INFO" +ZONE_TX_END = "--END ZONE_TXN_INFO" +ZONE_ERROR_CODE = "errorCode" +ZONE_PAGE = "/gzoneinfo.htm" +CFG_NAME = "openstack_cfg" +ZONE_STRING_PREFIX = "zonecfginfo=" +ZONE_ERROR_MSG = "errorMessage" +ZONE_TX_ID = "txnId" +ZONE_TX_STATUS = "status" +SESSION_LF_ID = "sessionLFId" +HTTP = "http" +HTTPS = "https" diff --git a/releasenotes/notes/brocade_http_connector-0021e41dfa56e671.yaml b/releasenotes/notes/brocade_http_connector-0021e41dfa56e671.yaml new file mode 100644 index 000000000..12fb93815 --- /dev/null +++ b/releasenotes/notes/brocade_http_connector-0021e41dfa56e671.yaml @@ -0,0 +1,10 @@ +--- +features: + - HTTP connector for the Cinder Brocade FC Zone plugin. + This connector allows for communication + between the Brocade FC zone plugin and the switch + to be over HTTP or HTTPs. To make use of this + connector, the user would add a configuration + setting in the fabric block for a Brocade switch + with the name as 'fc_southbound_protocol' with + a value as 'HTTP' or 'HTTPS'. -- 2.45.2