Add Cisco FC Zoning plugin to the FC ZoneManager

The Cisco FC Zoning plugin allows for the automated creation,
deletion and modification of zones in zonesets.  The Cisco
FC zoning plugin supports both basic and enhanced zoning and
supports vsans.

Implements: blueprint cisco-fc-zone-driver
Change-Id: Icf176b6f46f1b1662f0a99ffaa226789661f611e
This commit is contained in:
John McDonough 2014-04-02 18:47:06 -04:00
parent 387afbbd58
commit 230f115d34
13 changed files with 2140 additions and 0 deletions

View File

@ -773,6 +773,10 @@ class BrocadeZoningCliException(CinderException):
message = _("Fibre Channel Zoning CLI error: %(reason)s")
class CiscoZoningCliException(CinderException):
message = _("Fibre Channel Zoning CLI error: %(reason)s")
class NetAppDriverException(VolumeDriverException):
message = _("NetApp Cinder Driver exception.")

View File

@ -0,0 +1,133 @@
# (c) Copyright 2014 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Unit tests for Cisco fc san lookup service."""
import mock
from oslo.config import cfg
from cinder import exception
from cinder import test
from cinder.volume import configuration as conf
import cinder.zonemanager.drivers.cisco.cisco_fc_san_lookup_service \
as cisco_lookup
from cinder.zonemanager.utils import get_formatted_wwn
nsshow = '20:1a:00:05:1e:e8:e3:29'
switch_data = ['VSAN 304\n',
'------------------------------------------------------\n',
'FCID TYPE PWWN (VENDOR) \n',
'------------------------------------------------------\n',
'0x030001 N 20:1a:00:05:1e:e8:e3:29 (Cisco) ipfc\n',
'0x030101 NL 10:00:00:00:77:99:60:2c (Interphase)\n',
'0x030200 N 10:00:00:49:c9:28:c7:01\n']
nsshow_data = ['10:00:8c:7c:ff:52:3b:01', '20:24:00:02:ac:00:0a:50']
_device_map_to_verify = {
'304': {
'initiator_port_wwn_list': ['10008c7cff523b01'],
'target_port_wwn_list': ['20240002ac000a50']}}
class TestCiscoFCSanLookupService(cisco_lookup.CiscoFCSanLookupService,
test.TestCase):
def setUp(self):
super(TestCiscoFCSanLookupService, self).setUp()
self.configuration = conf.Configuration(None)
self.configuration.set_default('fc_fabric_names', 'CISCO_FAB_2',
'fc-zone-manager')
self.configuration.fc_fabric_names = 'CISCO_FAB_2'
self.create_configuration()
# override some of the functions
def __init__(self, *args, **kwargs):
test.TestCase.__init__(self, *args, **kwargs)
def create_configuration(self):
fc_fabric_opts = []
fc_fabric_opts.append(cfg.StrOpt('cisco_fc_fabric_address',
default='172.24.173.142', help=''))
fc_fabric_opts.append(cfg.StrOpt('cisco_fc_fabric_user',
default='admin', help=''))
fc_fabric_opts.append(cfg.StrOpt('cisco_fc_fabric_password',
default='admin1234', help='',
secret=True))
fc_fabric_opts.append(cfg.IntOpt('cisco_fc_fabric_port',
default=22, help=''))
fc_fabric_opts.append(cfg.StrOpt('cisco_zoning_vsan',
default='304', help=''))
config = conf.Configuration(fc_fabric_opts, 'CISCO_FAB_2')
self.fabric_configs = {'CISCO_FAB_2': config}
@mock.patch.object(cisco_lookup.CiscoFCSanLookupService,
'get_nameserver_info')
def test_get_device_mapping_from_network(self, get_nameserver_info_mock):
initiator_list = ['10008c7cff523b01']
target_list = ['20240002ac000a50', '20240002ac000a40']
get_nameserver_info_mock.return_value = (nsshow_data)
device_map = self.get_device_mapping_from_network(
initiator_list, target_list)
self.assertDictMatch(device_map, _device_map_to_verify)
@mock.patch.object(cisco_lookup.CiscoFCSanLookupService,
'_get_switch_info')
def test_get_nameserver_info(self, get_switch_data_mock):
ns_info_list = []
ns_info_list_expected = ['20:1a:00:05:1e:e8:e3:29',
'10:00:00:49:c9:28:c7:01']
get_switch_data_mock.return_value = (switch_data)
ns_info_list = self.get_nameserver_info('304')
self.assertEqual(ns_info_list, ns_info_list_expected)
def test_parse_ns_output(self):
invalid_switch_data = [' N 011a00;20:1a:00:05:1e:e8:e3:29']
return_wwn_list = []
expected_wwn_list = ['20:1a:00:05:1e:e8:e3:29',
'10:00:00:49:c9:28:c7:01']
return_wwn_list = self._parse_ns_output(switch_data)
self.assertEqual(return_wwn_list, expected_wwn_list)
self.assertRaises(exception.InvalidParameterValue,
self._parse_ns_output, invalid_switch_data)
def test_get_formatted_wwn(self):
wwn_list = ['10008c7cff523b01']
return_wwn_list = []
expected_wwn_list = ['10:00:8c:7c:ff:52:3b:01']
return_wwn_list.append(get_formatted_wwn(wwn_list[0]))
self.assertEqual(return_wwn_list, expected_wwn_list)
class Channel(object):
def recv_exit_status(self):
return 0
class Stream(object):
def __init__(self, buffer=''):
self.buffer = buffer
self.channel = Channel()
def readlines(self):
return self.buffer
def close(self):
pass
def flush(self):
self.buffer = ''

View File

@ -0,0 +1,233 @@
# (c) Copyright 2014 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Unit tests for Cisco fc zone client cli."""
from mock import patch
from cinder import exception
from cinder.openstack.common import processutils
from cinder import test
from cinder.zonemanager.drivers.cisco.cisco_fc_zone_client_cli \
import CiscoFCZoneClientCLI
import cinder.zonemanager.drivers.cisco.fc_zone_constants as ZoneConstant
nsshow = '20:1a:00:05:1e:e8:e3:29'
switch_data = ['VSAN 303\n',
'----------------------------------------------------------\n',
'FCID TYPE PWWN (VENDOR) FC4-TYPE:FEATURE\n',
'----------------------------------------------------------\n',
'0x030001 N 20:1a:00:05:1e:e8:e3:29 (Cisco) ipfc\n',
'0x030101 NL 10:00:00:00:77:99:60:2c (Interphase)\n',
'0x030200 NL 10:00:00:49:c9:28:c7:01\n']
cfgactv = ['zoneset name OpenStack_Cfg vsan 303\n',
'zone name openstack50060b0000c26604201900051ee8e329 vsan 303\n',
'pwwn 50:06:0b:00:00:c2:66:04\n',
'pwwn 20:19:00:05:1e:e8:e3:29\n']
active_zoneset = {
'zones': {
'openstack50060b0000c26604201900051ee8e329':
['50:06:0b:00:00:c2:66:04', '20:19:00:05:1e:e8:e3:29']},
'active_zone_config': 'OpenStack_Cfg'}
zoning_status_data_basic = [
'VSAN: 303 default-zone: deny distribute: active only Interop: default\n',
' mode: basic merge-control: allow\n',
' session: none\n',
' hard-zoning: enabled broadcast: unsupported\n',
' smart-zoning: disabled\n',
' rscn-format: fabric-address\n',
'Default zone:\n',
' qos: none broadcast: unsupported ronly: unsupported\n',
'Full Zoning Database :\n',
' DB size: 220 bytes\n',
' Zonesets:2 Zones:2 Aliases: 0\n',
'Active Zoning Database :\n',
' DB size: 80 bytes\n',
' Name: test-zs-test Zonesets:1 Zones:1\n',
'Status:\n']
zoning_status_basic = {'mode': 'basic', 'session': 'none'}
zoning_status_data_enhanced_nosess = [
'VSAN: 303 default-zone: deny distribute: active only Interop: default\n',
' mode: enhanced merge-control: allow\n',
' session: none\n',
' hard-zoning: enabled broadcast: unsupported\n',
' smart-zoning: disabled\n',
' rscn-format: fabric-address\n',
'Default zone:\n',
' qos: none broadcast: unsupported ronly: unsupported\n',
'Full Zoning Database :\n',
' DB size: 220 bytes\n',
' Zonesets:2 Zones:2 Aliases: 0\n',
'Active Zoning Database :\n',
' DB size: 80 bytes\n',
' Name: test-zs-test Zonesets:1 Zones:1\n',
'Status:\n']
zoning_status_enhanced_nosess = {'mode': 'enhanced', 'session': 'none'}
zoning_status_data_enhanced_sess = [
'VSAN: 303 default-zone: deny distribute: active only Interop: default\n',
' mode: enhanced merge-control: allow\n',
' session: otherthannone\n',
' hard-zoning: enabled broadcast: unsupported\n',
' smart-zoning: disabled\n',
' rscn-format: fabric-address\n',
'Default zone:\n',
' qos: none broadcast: unsupported ronly: unsupported\n',
'Full Zoning Database :\n',
' DB size: 220 bytes\n',
' Zonesets:2 Zones:2 Aliases: 0\n',
'Active Zoning Database :\n',
' DB size: 80 bytes\n',
' Name: test-zs-test Zonesets:1 Zones:1\n',
'Status:\n']
zoning_status_enhanced_sess = {'mode': 'enhanced', 'session': 'otherthannone'}
active_zoneset_multiple_zones = {
'zones': {
'openstack50060b0000c26604201900051ee8e329':
['50:06:0b:00:00:c2:66:04', '20:19:00:05:1e:e8:e3:29'],
'openstack50060b0000c26602201900051ee8e327':
['50:06:0b:00:00:c2:66:02', '20:19:00:05:1e:e8:e3:27']},
'active_zone_config': 'OpenStack_Cfg'}
new_zone = {'openstack10000012345678902001009876543210':
['10:00:00:12:34:56:78:90', '20:01:00:98:76:54:32:10']}
new_zones = {'openstack10000012345678902001009876543210':
['10:00:00:12:34:56:78:90', '20:01:00:98:76:54:32:10'],
'openstack10000011111111112001001111111111':
['10:00:00:11:11:11:11:11', '20:01:00:11:11:11:11:11']}
zone_names_to_delete = 'openstack50060b0000c26604201900051ee8e329'
class TestCiscoFCZoneClientCLI(CiscoFCZoneClientCLI, test.TestCase):
def setUp(self):
super(TestCiscoFCZoneClientCLI, self).setUp()
self.fabric_vsan = '303'
# override some of the functions
def __init__(self, *args, **kwargs):
test.TestCase.__init__(self, *args, **kwargs)
@patch.object(CiscoFCZoneClientCLI, '_get_switch_info')
def test_get_active_zone_set(self, get_switch_info_mock):
cmd_list = [ZoneConstant.GET_ACTIVE_ZONE_CFG, self.fabric_vsan,
' | no-more']
get_switch_info_mock.return_value = cfgactv
active_zoneset_returned = self.get_active_zone_set()
get_switch_info_mock.assert_called_once_with(cmd_list)
self.assertDictMatch(active_zoneset_returned, active_zoneset)
@patch.object(CiscoFCZoneClientCLI, '_run_ssh')
def test_get_active_zone_set_ssh_error(self, run_ssh_mock):
run_ssh_mock.side_effect = processutils.ProcessExecutionError
self.assertRaises(exception.CiscoZoningCliException,
self.get_active_zone_set)
@patch.object(CiscoFCZoneClientCLI, '_get_switch_info')
def test_get_zoning_status_basic(self, get_zoning_status_mock):
cmd_list = [ZoneConstant.GET_ZONE_STATUS, self.fabric_vsan]
get_zoning_status_mock.return_value = zoning_status_data_basic
zoning_status_returned = self.get_zoning_status()
get_zoning_status_mock.assert_called_once_with(cmd_list)
self.assertDictMatch(zoning_status_returned, zoning_status_basic)
@patch.object(CiscoFCZoneClientCLI, '_get_switch_info')
def test_get_zoning_status_enhanced_nosess(self, get_zoning_status_mock):
cmd_list = [ZoneConstant.GET_ZONE_STATUS, self.fabric_vsan]
get_zoning_status_mock.return_value =\
zoning_status_data_enhanced_nosess
zoning_status_returned = self.get_zoning_status()
get_zoning_status_mock.assert_called_once_with(cmd_list)
self.assertDictMatch(zoning_status_returned,
zoning_status_enhanced_nosess)
@patch.object(CiscoFCZoneClientCLI, '_get_switch_info')
def test_get_zoning_status_enhanced_sess(self, get_zoning_status_mock):
cmd_list = [ZoneConstant.GET_ZONE_STATUS, self.fabric_vsan]
get_zoning_status_mock.return_value = zoning_status_data_enhanced_sess
zoning_status_returned = self.get_zoning_status()
get_zoning_status_mock.assert_called_once_with(cmd_list)
self.assertDictMatch(zoning_status_returned,
zoning_status_enhanced_sess)
@patch.object(CiscoFCZoneClientCLI, '_get_switch_info')
def test_get_nameserver_info(self, get_switch_info_mock):
ns_info_list = []
ns_info_list_expected = ['20:1a:00:05:1e:e8:e3:29']
get_switch_info_mock.return_value = (switch_data)
ns_info_list = self.get_nameserver_info()
self.assertEqual(ns_info_list, ns_info_list_expected)
@patch.object(CiscoFCZoneClientCLI, '_run_ssh')
def test_get_nameserver_info_ssh_error(self, run_ssh_mock):
run_ssh_mock.side_effect = processutils.ProcessExecutionError
self.assertRaises(exception.CiscoZoningCliException,
self.get_nameserver_info)
@patch.object(CiscoFCZoneClientCLI, '_run_ssh')
def test__cfg_save(self, run_ssh_mock):
cmd_list = ['copy', 'running-config', 'startup-config']
self._cfg_save()
run_ssh_mock.assert_called_once_with(cmd_list, True, 1)
@patch.object(CiscoFCZoneClientCLI, '_run_ssh')
def test__get_switch_info(self, run_ssh_mock):
cmd_list = [ZoneConstant.FCNS_SHOW, self.fabric_vsan]
nsshow_list = [nsshow]
run_ssh_mock.return_value = (Stream(nsshow), Stream())
switch_data = self._get_switch_info(cmd_list)
self.assertEqual(switch_data, nsshow_list)
run_ssh_mock.assert_called_once_with(cmd_list, True, 1)
def test__parse_ns_output(self):
return_wwn_list = []
expected_wwn_list = ['20:1a:00:05:1e:e8:e3:29']
return_wwn_list = self._parse_ns_output(switch_data)
self.assertEqual(return_wwn_list, expected_wwn_list)
class Channel(object):
def recv_exit_status(self):
return 0
class Stream(object):
def __init__(self, buffer=''):
self.buffer = buffer
self.channel = Channel()
def readlines(self):
return self.buffer
def splitlines(self):
return self.buffer.splitlines()
def close(self):
pass
def flush(self):
self.buffer = ''

View File

@ -0,0 +1,208 @@
# (c) Copyright 2014 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Unit tests for Cisco FC zone driver."""
from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import importutils
from cinder.openstack.common import processutils
from cinder import test
from cinder.volume import configuration as conf
_active_cfg_before_add = {}
_active_cfg_before_delete = {
'zones': {
'openstack10008c7cff523b0120240002ac000a50': (
['10:00:8c:7c:ff:52:3b:01',
'20:24:00:02:ac:00:0a:50'])},
'active_zone_config': 'cfg1'}
_activate = True
_zone_name = 'openstack10008c7cff523b0120240002ac000a50'
_target_ns_map = {'100000051e55a100': ['20240002ac000a50']}
_zoning_status = {'mode': 'basis', 'session': 'none'}
_initiator_ns_map = {'100000051e55a100': ['10008c7cff523b01']}
_zone_map_to_add = {'openstack10008c7cff523b0120240002ac000a50': (
['10:00:8c:7c:ff:52:3b:01', '20:24:00:02:ac:00:0a:50'])}
_initiator_target_map = {'10008c7cff523b01': ['20240002ac000a50']}
_device_map_to_verify = {
'304': {
'initiator_port_wwn_list': [
'10008c7cff523b01'], 'target_port_wwn_list': ['20240002ac000a50']}}
_fabric_wwn = '304'
class CiscoFcZoneDriverBaseTest(object):
def setup_config(self, is_normal, mode):
fc_test_opts = [
cfg.StrOpt('fc_fabric_address_CISCO_FAB_1', default='10.24.48.213',
help='FC Fabric names'),
]
configuration = conf.Configuration(fc_test_opts)
# fill up config
configuration.zoning_mode = 'fabric'
configuration.zone_driver = ('cinder.tests.zonemanager.'
'test_cisco_fc_zone_driver.'
'FakeCiscoFCZoneDriver')
configuration.cisco_sb_connector = ('cinder.tests.zonemanager.'
'test_cisco_fc_zone_driver'
'.FakeCiscoFCZoneClientCLI')
configuration.zoning_policy = 'initiator-target'
configuration.zone_activate = True
configuration.zone_name_prefix = 'openstack'
configuration.fc_san_lookup_service = ('cinder.tests.zonemanager.'
'test_cisco_fc_zone_driver.'
'FakeCiscoFCSanLookupService')
configuration.fc_fabric_names = 'CISCO_FAB_1'
configuration.fc_fabric_address_CISCO_FAB_1 = '172.21.60.220'
if (is_normal):
configuration.fc_fabric_user_CISCO_FAB_1 = 'admin'
else:
configuration.fc_fabric_user_CISCO_FAB_1 = 'invaliduser'
configuration.fc_fabric_password_CISCO_FAB_1 = 'admin1234'
if (mode == 1):
configuration.zoning_policy_CISCO_FAB_1 = 'initiator-target'
elif (mode == 2):
configuration.zoning_policy_CISCO_FAB_1 = 'initiator'
else:
configuration.zoning_policy_CISCO_FAB_1 = 'initiator-target'
configuration.zone_activate_CISCO_FAB_1 = True
configuration.zone_name_prefix_CISCO_FAB_1 = 'openstack'
configuration.zoning_vsan_CISCO_FAB_1 = '304'
return configuration
class TestCiscoFcZoneDriver(CiscoFcZoneDriverBaseTest, test.TestCase):
def setUp(self):
super(TestCiscoFcZoneDriver, self).setUp()
# setup config for normal flow
self.setup_driver(self.setup_config(True, 1))
GlobalVars._zone_state = []
def setup_driver(self, config):
self.driver = importutils.import_object(
'cinder.zonemanager.drivers.cisco.cisco_fc_zone_driver'
'.CiscoFCZoneDriver', configuration=config)
def fake_get_active_zone_set(self, fabric_ip, fabric_user, fabric_pwd,
zoning_vsan):
return GlobalVars._active_cfg
def fake_get_san_context(self, target_wwn_list):
fabric_map = {}
return fabric_map
def test_delete_connection(self):
GlobalVars._is_normal_test = True
GlobalVars._active_cfg = _active_cfg_before_delete
self.driver.delete_connection(
'CISCO_FAB_1', _initiator_target_map)
self.assertFalse(_zone_name in GlobalVars._zone_state)
def test_delete_connection_for_initiator_mode(self):
GlobalVars._is_normal_test = True
GlobalVars._active_cfg = _active_cfg_before_delete
self.setup_driver(self.setup_config(True, 2))
self.driver.delete_connection(
'CISCO_FAB_1', _initiator_target_map)
self.assertFalse(_zone_name in GlobalVars._zone_state)
def test_add_connection_for_invalid_fabric(self):
"""Test abnormal flows."""
GlobalVars._is_normal_test = True
GlobalVars._active_cfg = _active_cfg_before_add
GlobalVars._is_normal_test = False
self.setup_driver(self.setup_config(False, 1))
self.assertRaises(exception.FCZoneDriverException,
self.driver.add_connection,
'CISCO_FAB_1',
_initiator_target_map)
def test_delete_connection_for_invalid_fabric(self):
GlobalVars._active_cfg = _active_cfg_before_delete
GlobalVars._is_normal_test = False
self.setup_driver(self.setup_config(False, 1))
self.assertRaises(exception.FCZoneDriverException,
self.driver.delete_connection,
'CISCO_FAB_1',
_initiator_target_map)
class FakeCiscoFCZoneClientCLI(object):
def __init__(self, ipaddress, username, password, port, vsan):
if not GlobalVars._is_normal_test:
raise processutils.ProcessExecutionError(
"Unable to connect to fabric")
def get_active_zone_set(self):
return GlobalVars._active_cfg
def add_zones(self, zones, isActivate):
GlobalVars._zone_state.extend(zones.keys())
def delete_zones(self, zone_names, isActivate):
zone_list = zone_names.split(';')
GlobalVars._zone_state = [
x for x in GlobalVars._zone_state if x not in zone_list]
def get_nameserver_info(self):
return _target_ns_map
def get_zoning_status(self):
return _zoning_status
def close_connection(self):
pass
def cleanup(self):
pass
class FakeCiscoFCSanLookupService(object):
def get_device_mapping_from_network(self,
initiator_wwn_list,
target_wwn_list):
device_map = {}
initiators = []
targets = []
for i in initiator_wwn_list:
if (i in _initiator_ns_map[_fabric_wwn]):
initiators.append(i)
for t in target_wwn_list:
if (t in _target_ns_map[_fabric_wwn]):
targets.append(t)
device_map[_fabric_wwn] = {
'initiator_port_wwn_list': initiators,
'target_port_wwn_list': targets}
return device_map
class GlobalVars(object):
global _active_cfg
_active_cfg = {}
global _zone_state
_zone_state = list()
global _is_normal_test
_is_normal_test = True
global _zoning_status
_zoning_status = {}

View File

@ -0,0 +1,96 @@
# (c) Copyright 2014 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Unit tests for Cisco FC san lookup service."""
from cinder import exception
from cinder import test
from cinder.volume import configuration as conf
from cinder.zonemanager.fc_san_lookup_service import FCSanLookupService
_target_ns_map = {'100000051e55a100': ['20240002ac000a50']}
_initiator_ns_map = {'100000051e55a100': ['10008c7cff523b01']}
_device_map_to_verify = {
'100000051e55a100': {
'initiator_port_wwn_list': [
'10008c7cff523b01'], 'target_port_wwn_list': ['20240002ac000a50']}}
_fabric_wwn = '100000051e55a100'
class TestFCSanLookupService(FCSanLookupService, test.TestCase):
def setUp(self):
super(TestFCSanLookupService, self).setUp()
self.configuration = self.setup_config()
# override some of the functions
def __init__(self, *args, **kwargs):
test.TestCase.__init__(self, *args, **kwargs)
def setup_config(self):
configuration = conf.Configuration(None)
# fill up config
configuration.fc_san_lookup_service = ('cinder.tests.zonemanager'
'.test_cisco_lookup_service'
'.FakeCiscoFCSanLookupService')
return configuration
def test_get_device_mapping_from_network(self):
GlobalParams._is_normal_test = True
initiator_list = ['10008c7cff523b01']
target_list = ['20240002ac000a50', '20240002ac000a40']
device_map = self.get_device_mapping_from_network(
initiator_list, target_list)
self.assertDictMatch(device_map, _device_map_to_verify)
def test_get_device_mapping_from_network_for_invalid_config(self):
GlobalParams._is_normal_test = False
initiator_list = ['10008c7cff523b01']
target_list = ['20240002ac000a50', '20240002ac000a40']
self.assertRaises(exception.FCSanLookupServiceException,
self.get_device_mapping_from_network,
initiator_list, target_list)
class FakeCiscoFCSanLookupService(object):
def __init__(self, **kwargs):
pass
def get_device_mapping_from_network(self,
initiator_wwn_list,
target_wwn_list):
if not GlobalParams._is_normal_test:
raise exception.FCSanLookupServiceException("Error")
device_map = {}
initiators = []
targets = []
for i in initiator_wwn_list:
if (i in _initiator_ns_map[_fabric_wwn]):
initiators.append(i)
for t in target_wwn_list:
if (t in _target_ns_map[_fabric_wwn]):
targets.append(t)
device_map[_fabric_wwn] = {
'initiator_port_wwn_list': initiators,
'target_port_wwn_list': targets}
return device_map
class GlobalParams(object):
global _is_normal_test
_is_normal_test = True

View File

@ -0,0 +1,58 @@
# (c) Copyright 2014 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from oslo.config import cfg
from cinder.volume.configuration import Configuration
cisco_zone_opts = [
cfg.StrOpt('cisco_fc_fabric_address',
default='',
help='Management IP of fabric'),
cfg.StrOpt('cisco_fc_fabric_user',
default='',
help='Fabric user ID'),
cfg.StrOpt('cisco_fc_fabric_password',
default='',
help='Password for user',
secret=True),
cfg.IntOpt('cisco_fc_fabric_port',
default=22,
help='Connecting port'),
cfg.StrOpt('cisco_zoning_policy',
default='initiator-target',
help='overridden zoning policy'),
cfg.BoolOpt('cisco_zone_activate',
default=True,
help='overridden zoning activation state'),
cfg.StrOpt('cisco_zone_name_prefix',
default=None,
help='overridden zone name prefix'),
cfg.StrOpt('cisco_zoning_vsan',
default=None,
help='VSAN of the Fabric'),
]
CONF = cfg.CONF
CONF.register_opts(cisco_zone_opts, 'CISCO_FABRIC_EXAMPLE')
def load_fabric_configurations(fabric_names):
fabric_configs = {}
for fabric_name in fabric_names:
config = Configuration(cisco_zone_opts, fabric_name)
fabric_configs[fabric_name] = config
return fabric_configs

View File

@ -0,0 +1,357 @@
# (c) Copyright 2014 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import random
from eventlet import greenthread
import six
from cinder import exception
from cinder.i18n import _
from cinder.openstack.common import excutils
from cinder.openstack.common import log as logging
from cinder.openstack.common import processutils
from cinder import ssh_utils
from cinder import utils
from cinder.zonemanager.drivers.cisco import cisco_fabric_opts as fabric_opts
import cinder.zonemanager.drivers.cisco.fc_zone_constants as ZoneConstant
from cinder.zonemanager.fc_san_lookup_service import FCSanLookupService
from cinder.zonemanager.utils import get_formatted_wwn
LOG = logging.getLogger(__name__)
class CiscoFCSanLookupService(FCSanLookupService):
"""The SAN lookup service that talks to Cisco switches.
Version History:
1.0.0 - Initial version
"""
VERSION = "1.0.0"
def __init__(self, **kwargs):
"""Initializing the client."""
super(CiscoFCSanLookupService, self).__init__(self, **kwargs)
self.configuration = kwargs.get('configuration', None)
self.create_configuration()
self.switch_user = ""
self.switch_port = ""
self.switch_pwd = ""
self.switch_ip = ""
self.sshpool = None
self.fabric_configs = ""
def create_configuration(self):
"""Configuration specific to SAN context values."""
config = self.configuration
fabric_names = [x.strip() for x in config.fc_fabric_names.split(',')]
LOG.debug('Fabric Names: %s', fabric_names)
# There can be more than one SAN in the network and we need to
# get credentials for each for SAN context lookup later.
# Cisco Zonesets require VSANs
if fabric_names:
self.fabric_configs = fabric_opts.load_fabric_configurations(
fabric_names)
def get_device_mapping_from_network(self,
initiator_wwn_list,
target_wwn_list):
"""Provides the initiator/target map for available SAN contexts.
Looks up fcns database of each fc SAN configured to find logged in
devices and returns a map of initiator and target port WWNs for each
fabric.
:param initiator_wwn_list: List of initiator port WWN
:param target_wwn_list: List of target port WWN
:returns List -- device wwn map in following format
{
<San name>: {
'initiator_port_wwn_list':
('200000051e55a100', '200000051e55a121'..)
'target_port_wwn_list':
('100000051e55a100', '100000051e55a121'..)
}
}
:raises Exception when connection to fabric is failed
"""
device_map = {}
formatted_target_list = []
formatted_initiator_list = []
fabric_map = {}
fabric_names = self.configuration.fc_fabric_names
if not fabric_names:
raise exception.InvalidParameterValue(
err=_("Missing Fibre Channel SAN configuration "
"param - fc_fabric_names"))
fabrics = [x.strip() for x in fabric_names.split(',')]
LOG.debug("FC Fabric List: %s", fabrics)
if fabrics:
for t in target_wwn_list:
formatted_target_list.append(get_formatted_wwn(t))
for i in initiator_wwn_list:
formatted_initiator_list.append(get_formatted_wwn(i))
for fabric_name in fabrics:
self.switch_ip = self.fabric_configs[fabric_name].safe_get(
'cisco_fc_fabric_address')
self.switch_user = self.fabric_configs[fabric_name].safe_get(
'cisco_fc_fabric_user')
self.switch_pwd = self.fabric_configs[fabric_name].safe_get(
'cisco_fc_fabric_password')
self.switch_port = self.fabric_configs[fabric_name].safe_get(
'cisco_fc_fabric_port')
zoning_vsan = self.fabric_configs[fabric_name].safe_get(
'cisco_zoning_vsan')
# Get name server data from fabric and find the targets
# logged in
nsinfo = ''
LOG.debug("show fcns database for vsan %s", zoning_vsan)
nsinfo = self.get_nameserver_info(zoning_vsan)
LOG.debug("Lookup service:fcnsdatabase-%s", nsinfo)
LOG.debug("Lookup service:initiator list from caller-%s",
formatted_initiator_list)
LOG.debug("Lookup service:target list from caller-%s",
formatted_target_list)
visible_targets = filter(lambda x: x in formatted_target_list,
nsinfo)
visible_initiators = filter(lambda x: x in
formatted_initiator_list, nsinfo)
if visible_targets:
LOG.debug("Filtered targets is: %s", visible_targets)
# getting rid of the : before returning
for idx, elem in enumerate(visible_targets):
elem = str(elem).replace(':', '')
visible_targets[idx] = elem
else:
LOG.debug("No targets are in the fcns database"
" for vsan %s", zoning_vsan)
if visible_initiators:
# getting rid of the : before returning ~sk
for idx, elem in enumerate(visible_initiators):
elem = str(elem).replace(':', '')
visible_initiators[idx] = elem
else:
LOG.debug("No initiators are in the fcns database"
" for vsan %s", zoning_vsan)
fabric_map = {'initiator_port_wwn_list': visible_initiators,
'target_port_wwn_list': visible_targets
}
device_map[zoning_vsan] = fabric_map
LOG.debug("Device map for SAN context: %s", device_map)
return device_map
def get_nameserver_info(self, fabric_vsan):
"""Get fcns database info from fabric.
This method will return the connected node port wwn list(local
and remote) for the given switch fabric
"""
cli_output = None
nsinfo_list = []
try:
cmd = ZoneConstant.FCNS_SHOW + fabric_vsan + ' | no-more'
cli_output = self._get_switch_info(cmd)
except exception.FCSanLookupServiceException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed collecting show fcns database for"
" fabric"))
if cli_output:
nsinfo_list = self._parse_ns_output(cli_output)
LOG.debug("Connector returning fcns info-%s", nsinfo_list)
return nsinfo_list
def _get_switch_info(self, cmd_list):
stdout, stderr, sw_data = None, None, None
try:
stdout, stderr = self._run_ssh(cmd_list, True, 1)
LOG.debug("CLI output from ssh - output:%s", stdout)
if (stdout):
sw_data = stdout.splitlines()
return sw_data
except processutils.ProcessExecutionError as e:
msg = _("Error while getting data via ssh: (command=%(cmd)s "
"error=%(err)s).") % {'cmd': cmd_list,
'err': six.text_type(e)}
LOG.error(msg)
raise exception.CiscoZoningCliException(reason=msg)
def _parse_ns_output(self, switch_data):
"""Parses name server data.
Parses nameserver raw data and adds the device port wwns to the list
:returns list of device port wwn from ns info
"""
nsinfo_list = []
for line in switch_data:
if not(" N " in line):
continue
linesplit = line.split()
if len(linesplit) > 2:
node_port_wwn = linesplit[2]
nsinfo_list.append(node_port_wwn)
else:
msg = _("Malformed fcns output string: %s") % line
LOG.error(msg)
raise exception.InvalidParameterValue(err=msg)
return nsinfo_list
def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1):
command = ' '.join(cmd_list)
if not self.sshpool:
self.sshpool = ssh_utils.SSHPool(self.switch_ip,
self.switch_port,
None,
self.switch_user,
self.switch_pwd,
min_size=1,
max_size=5)
last_exception = None
try:
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
return processutils.ssh_execute(
ssh,
command,
check_exit_code=check_exit_code)
except Exception as e:
msg = _("Exception: %s") % six.text_type(e)
LOG.error(msg)
last_exception = e
greenthread.sleep(random.randint(20, 500) / 100.0)
try:
raise processutils.ProcessExecutionError(
exit_code=last_exception.exit_code,
stdout=last_exception.stdout,
stderr=last_exception.stderr,
cmd=last_exception.cmd)
except AttributeError:
raise processutils.ProcessExecutionError(
exit_code=-1,
stdout="",
stderr="Error running SSH command",
cmd=command)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_("Error running SSH command: %s") % command)
def _ssh_execute(self, cmd_list, check_exit_code=True, attempts=1):
"""Execute cli with status update.
Executes CLI commands where status return is expected.
cmd_list is a list of commands, where each command is itself
a list of parameters. We use utils.check_ssh_injection to check each
command, but then join then with " ; " to form a single command.
"""
# Check that each command is secure
for cmd in cmd_list:
utils.check_ssh_injection(cmd)
# Combine into a single command.
command = ' ; '.join(map(lambda x: ' '.join(x), cmd_list))
if not self.sshpool:
self.sshpool = ssh_utils.SSHPool(self.switch_ip,
self.switch_port,
None,
self.switch_user,
self.switch_pwd,
min_size=1,
max_size=5)
stdin, stdout, stderr = None, None, None
LOG.debug("Executing command via ssh: %s" % command)
last_exception = None
try:
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
stdin, stdout, stderr = ssh.exec_command(command)
greenthread.sleep(random.randint(20, 500) / 100.0)
channel = stdout.channel
exit_status = channel.recv_exit_status()
LOG.debug("Exit Status from ssh:%s", exit_status)
# exit_status == -1 if no exit code was returned
if exit_status != -1:
LOG.debug('Result was %s' % exit_status)
if check_exit_code and exit_status != 0:
raise processutils.ProcessExecutionError(
exit_code=exit_status,
stdout=stdout,
stderr=stderr,
cmd=command)
else:
return True
else:
return True
except Exception as e:
msg = _("Exception: %s") % six.text_type(e)
LOG.error(msg)
last_exception = e
greenthread.sleep(random.randint(20, 500) / 100.0)
LOG.debug("Handling error case after SSH:%s", last_exception)
try:
raise processutils.ProcessExecutionError(
exit_code=last_exception.exit_code,
stdout=last_exception.stdout,
stderr=last_exception.stderr,
cmd=last_exception.cmd)
except AttributeError:
raise processutils.ProcessExecutionError(
exit_code=-1,
stdout="",
stderr="Error running SSH command",
cmd=command)
except Exception as e:
with excutils.save_and_reraise_exception():
msg = (_("Error executing command via ssh: %s") %
six.text_type(e))
LOG.error(msg)
finally:
if stdin:
stdin.flush()
stdin.close()
if stdout:
stdout.close()
if stderr:
stderr.close()
def cleanup(self):
self.sshpool = None

View File

@ -0,0 +1,483 @@
# (c) Copyright 2014 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""
Script to push the zone configuration to Cisco SAN switches.
"""
import random
import re
from eventlet import greenthread
import six
from cinder import exception
from cinder.i18n import _
from cinder.openstack.common import excutils
from cinder.openstack.common import log as logging
from cinder.openstack.common import processutils
from cinder import ssh_utils
from cinder import utils
import cinder.zonemanager.drivers.cisco.fc_zone_constants as ZoneConstant
LOG = logging.getLogger(__name__)
class CiscoFCZoneClientCLI(object):
"""Cisco FC zone client cli implementation.
OpenStack Fibre Channel zone client cli connector
to manage FC zoning in Cisco SAN fabrics.
Version history:
1.0 - Initial Cisco FC zone client cli
"""
switch_ip = None
switch_port = '22'
switch_user = 'admin'
switch_pwd = 'none'
def __init__(self, ipaddress, username, password, port, vsan):
"""initializing the client."""
self.switch_ip = ipaddress
self.switch_port = port
self.switch_user = username
self.switch_pwd = password
self.fabric_vsan = vsan
self.sshpool = None
def get_active_zone_set(self):
"""Return the active zone configuration.
Return active zoneset from fabric. When none of the configurations
are active then it will return empty map.
:returns: Map -- active zone set map in the following format
{
'zones':
{'openstack50060b0000c26604201900051ee8e329':
['50060b0000c26604', '201900051ee8e329']
},
'active_zone_config': 'OpenStack_Cfg'
}
"""
zone_set = {}
zone = {}
zone_member = None
zone_name = None
switch_data = None
zone_set_name = None
try:
switch_data = self._get_switch_info(
[ZoneConstant.GET_ACTIVE_ZONE_CFG, self.fabric_vsan,
' | no-more'])
except exception.CiscoZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed getting active zone set "
"from fabric %s"), self.switch_ip)
try:
for line in switch_data:
# Split on non-word characters,
line_split = re.split('[\s\[\]]+', line)
if ZoneConstant.CFG_ZONESET in line_split:
# zoneset name [name] vsan [vsan]
zone_set_name = \
line_split[line_split.index(ZoneConstant.CFG_ZONESET)
+ 2]
continue
if ZoneConstant.CFG_ZONE in line_split:
# zone name [name] vsan [vsan]
zone_name = \
line_split[line_split.index(ZoneConstant.CFG_ZONE) + 2]
zone[zone_name] = list()
continue
if ZoneConstant.CFG_ZONE_MEMBER in line_split:
# Examples:
# pwwn c0:50:76:05:15:9f:00:12
# * fcid 0x1e01c0 [pwwn 50:05:07:68:02:20:48:04] [V7K_N1P2]
zone_member = \
line_split[
line_split.index(ZoneConstant.CFG_ZONE_MEMBER) + 1]
zone_member_list = zone.get(zone_name)
zone_member_list.append(zone_member)
zone_set[ZoneConstant.CFG_ZONES] = zone
zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG] = zone_set_name
except Exception as ex:
# In case of parsing error here, it should be malformed cli output.
msg = _("Malformed zone configuration: (switch=%(switch)s "
"zone_config=%(zone_config)s)."
) % {'switch': self.switch_ip,
'zone_config': switch_data}
LOG.error(msg)
exc_msg = _("Exception: %s") % six.text_type(ex)
LOG.exception(exc_msg)
raise exception.FCZoneDriverException(reason=msg)
return zone_set
def add_zones(self, zones, activate, fabric_vsan, active_zone_set,
zone_status):
"""Add zone configuration.
This method will add the zone configuration passed by user.
input params:
zones - zone names mapped to members and VSANs.
zone members are colon separated but case-insensitive
{ zonename1:[zonememeber1,zonemember2,...],
zonename2:[zonemember1, zonemember2,...]...}
e.g: {'openstack50060b0000c26604201900051ee8e329':
['50:06:0b:00:00:c2:66:04', '20:19:00:05:1e:e8:e3:29']
}
activate - True/False
"""
LOG.debug("Add Zones - Zones passed: %s", zones)
LOG.debug("Active zone set:%s", active_zone_set)
zone_list = active_zone_set[ZoneConstant.CFG_ZONES]
LOG.debug("zone list:%s", zone_list)
LOG.debug("zone status:%s", zone_status)
cfg_name = active_zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG]
zone_cmds = [['conf'],
['zoneset', 'name', cfg_name, 'vsan', fabric_vsan]]
for zone in zones.keys():
# if zone exists, its an update. Delete & insert
LOG.debug("Update call")
if zone in zone_list:
# Response from get_active_zone_set strips colons from WWPNs
current_zone = set(zone_list[zone])
new_wwpns = map(lambda x: x.lower().replace(':', ''),
zones[zone])
new_zone = set(new_wwpns)
if current_zone != new_zone:
try:
self.delete_zones([zone], activate, fabric_vsan,
active_zone_set, zone_status)
except exception.CiscoZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Deleting zone failed %s"), zone)
LOG.debug("Deleted Zone before insert : %s", zone)
zone_cmds.append(['zone', 'name', zone])
for member in zones[zone]:
zone_cmds.append(['member', 'pwwn', member])
zone_cmds.append(['end'])
try:
LOG.debug("Add zones: Config cmd to run:%s", zone_cmds)
self._ssh_execute(zone_cmds, True, 1)
if activate:
self.activate_zoneset(cfg_name, fabric_vsan, zone_status)
self._cfg_save()
except Exception as e:
msg = _("Creating and activating zone set failed: "
"(Zone set=%(zoneset)s error=%(err)s)."
) % {'zoneset': cfg_name, 'err': six.text_type(e)}
LOG.error(msg)
raise exception.CiscoZoningCliException(reason=msg)
def activate_zoneset(self, cfgname, fabric_vsan, zone_status):
"""Method to Activate the zone config. Param cfgname - ZonesetName."""
LOG.debug("zone status:%s", zone_status)
cmd_list = [['conf'],
['zoneset', 'activate', 'name', cfgname, 'vsan',
self.fabric_vsan]]
if zone_status['mode'] == 'enhanced':
cmd_list.append(['zone', 'commit', 'vsan', fabric_vsan])
cmd_list.append(['end'])
return self._ssh_execute(cmd_list, True, 1)
def get_zoning_status(self):
"""Return the zoning mode and session for a zoneset."""
zone_status = {}
try:
switch_data = self._get_switch_info(
[ZoneConstant.GET_ZONE_STATUS, self.fabric_vsan])
except exception.CiscoZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed getting zone status "
"from fabric %s"), self.switch_ip)
try:
for line in switch_data:
# Split on non-word characters,
line_split = re.split('[\s\[\]]+', line)
if 'mode:' in line_split:
# mode: <enhanced|basic>
zone_status['mode'] = line_split[line_split.index('mode:')
+ 1]
continue
if 'session:' in line_split:
# session: <none|a value other than none>
zone_status['session'] = \
line_split[line_split.index('session:') + 1]
continue
except Exception as ex:
# In case of parsing error here, it should be malformed cli output.
msg = _("Malformed zone status: (switch=%(switch)s "
"zone_config=%(zone_config)s)."
) % {'switch': self.switch_ip,
'zone_status': switch_data}
LOG.error(msg)
exc_msg = _("Exception: %s") % six.text_type(ex)
LOG.exception(exc_msg)
raise exception.FCZoneDriverException(reason=msg)
return zone_status
def delete_zones(self, zone_names, activate, fabric_vsan, active_zone_set,
zone_status):
"""Delete zones from fabric.
Method to delete the active zone config zones
params zone_names: zoneNames separated by semicolon
params activate: True/False
"""
LOG.debug("zone_names %s", zone_names)
active_zoneset_name = active_zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG]
cmds = [['conf'],
['zoneset', 'name', active_zoneset_name, 'vsan',
fabric_vsan]]
try:
for zone in set(zone_names.split(';')):
cmds.append(['no', 'zone', 'name', zone])
cmds.append(['end'])
LOG.debug("Delete zones: Config cmd to run:%s", cmds)
self._ssh_execute(cmds, True, 1)
if activate:
self.activate_zoneset(active_zoneset_name, fabric_vsan,
zone_status)
self._cfg_save()
except Exception as e:
msg = _("Deleting zones failed: (command=%(cmd)s error=%(err)s)."
) % {'cmd': cmds, 'err': six.text_type(e)}
LOG.error(msg)
raise exception.CiscoZoningCliException(reason=msg)
def get_nameserver_info(self):
"""Get name server data from fabric.
This method will return the connected node port wwn list(local
and remote) for the given switch fabric
show fcns database
"""
cli_output = None
return_list = []
try:
cli_output = self._get_switch_info([ZoneConstant.FCNS_SHOW,
self.fabric_vsan])
except exception.CiscoZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed collecting fcns database "
"info for fabric %s"), self.switch_ip)
if (cli_output):
return_list = self._parse_ns_output(cli_output)
LOG.info(_("Connector returning fcnsinfo-%s"), return_list)
return return_list
def _cfg_save(self):
cmd = ['copy', 'running-config', 'startup-config']
self._run_ssh(cmd, True, 1)
def _get_switch_info(self, cmd_list):
stdout, stderr, sw_data = None, None, None
try:
stdout, stderr = self._run_ssh(cmd_list, True, 1)
LOG.debug("CLI output from ssh - output:%s", stdout)
if (stdout):
sw_data = stdout.splitlines()
return sw_data
except processutils.ProcessExecutionError as e:
msg = _("Error while getting data via ssh: (command=%(cmd)s "
"error=%(err)s).") % {'cmd': cmd_list,
'err': six.text_type(e)}
LOG.error(msg)
raise exception.CiscoZoningCliException(reason=msg)
def _parse_ns_output(self, switch_data):
"""Parses name server data.
Parses nameserver raw data and adds the device port wwns to the list
:returns: List -- list of device port wwn from ns info
"""
return_list = []
for line in switch_data:
if not(" N " in line):
continue
linesplit = line.split()
if len(linesplit) > 2:
node_port_wwn = linesplit[2]
return_list.append(node_port_wwn)
else:
msg = _("Malformed show fcns database string: %s") % line
LOG.error(msg)
raise exception.InvalidParameterValue(err=msg)
return return_list
def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1):
command = ' '.join(cmd_list)
if not self.sshpool:
self.sshpool = ssh_utils.SSHPool(self.switch_ip,
self.switch_port,
None,
self.switch_user,
self.switch_pwd,
min_size=1,
max_size=5)
last_exception = None
try:
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
return processutils.ssh_execute(
ssh,
command,
check_exit_code=check_exit_code)
except Exception as e:
msg = _("Exception: %s") % six.text_type(e)
LOG.error(msg)
last_exception = e
greenthread.sleep(random.randint(20, 500) / 100.0)
try:
raise processutils.ProcessExecutionError(
exit_code=last_exception.exit_code,
stdout=last_exception.stdout,
stderr=last_exception.stderr,
cmd=last_exception.cmd)
except AttributeError:
raise processutils.ProcessExecutionError(
exit_code=-1,
stdout="",
stderr="Error running SSH command",
cmd=command)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_("Error running SSH command: %s") % command)
def _ssh_execute(self, cmd_list, check_exit_code=True, attempts=1):
"""Execute cli with status update.
Executes CLI commands where status return is expected.
cmd_list is a list of commands, where each command is itself
a list of parameters. We use utils.check_ssh_injection to check each
command, but then join then with " ; " to form a single command.
"""
# Check that each command is secure
for cmd in cmd_list:
utils.check_ssh_injection(cmd)
# Combine into a single command.
command = ' ; '.join(map(lambda x: ' '.join(x), cmd_list))
if not self.sshpool:
self.sshpool = ssh_utils.SSHPool(self.switch_ip,
self.switch_port,
None,
self.switch_user,
self.switch_pwd,
min_size=1,
max_size=5)
stdin, stdout, stderr = None, None, None
LOG.debug("Executing command via ssh: %s" % command)
last_exception = None
try:
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
stdin, stdout, stderr = ssh.exec_command(command)
greenthread.sleep(random.randint(20, 500) / 100.0)
channel = stdout.channel
exit_status = channel.recv_exit_status()
LOG.debug("Exit Status from ssh:%s", exit_status)
# exit_status == -1 if no exit code was returned
if exit_status != -1:
LOG.debug('Result was %s' % exit_status)
if check_exit_code and exit_status != 0:
raise processutils.ProcessExecutionError(
exit_code=exit_status,
stdout=stdout,
stderr=stderr,
cmd=command)
else:
return True
else:
return True
except Exception as e:
msg = _("Exception: %s") % six.text_type(e)
LOG.error(msg)
last_exception = e
greenthread.sleep(random.randint(20, 500) / 100.0)
LOG.debug("Handling error case after SSH:%s", last_exception)
try:
raise processutils.ProcessExecutionError(
exit_code=last_exception.exit_code,
stdout=last_exception.stdout,
stderr=last_exception.stderr,
cmd=last_exception.cmd)
except AttributeError:
raise processutils.ProcessExecutionError(
exit_code=-1,
stdout="",
stderr="Error running SSH command",
cmd=command)
except Exception as e:
with excutils.save_and_reraise_exception():
msg = (_("Error executing command via ssh: %s") %
six.text_type(e))
LOG.error(msg)
finally:
if stdin:
stdin.flush()
stdin.close()
if stdout:
stdout.close()
if stderr:
stderr.close()
def cleanup(self):
self.sshpool = None

View File

@ -0,0 +1,488 @@
# (c) Copyright 2014 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""
Cisco Zone Driver is responsible to manage access control using FC zoning
for Cisco FC fabrics.
This is a concrete implementation of FCZoneDriver interface implementing
add_connection and delete_connection interfaces.
**Related Flags**
:zone_activate: Used by: class: 'FCZoneDriver'. Defaults to True
:zone_name_prefix: Used by: class: 'FCZoneDriver'. Defaults to 'openstack'
"""
from oslo.config import cfg
import six
from cinder import exception
from cinder.i18n import _
from cinder.openstack.common import excutils
from cinder.openstack.common import importutils
from cinder.openstack.common import lockutils
from cinder.openstack.common import log as logging
from cinder.zonemanager.drivers.cisco import cisco_fabric_opts as fabric_opts
from cinder.zonemanager.drivers.fc_zone_driver import FCZoneDriver
from cinder.zonemanager.utils import get_formatted_wwn
LOG = logging.getLogger(__name__)
cisco_opts = [
cfg.StrOpt('cisco_sb_connector',
default='cinder.zonemanager.drivers.cisco'
'.cisco_fc_zone_client_cli.CiscoFCZoneClientCLI',
help='Southbound connector for zoning operation'),
]
CONF = cfg.CONF
CONF.register_opts(cisco_opts, 'fc-zone-manager')
class CiscoFCZoneDriver(FCZoneDriver):
"""Cisco FC zone driver implementation.
OpenStack Fibre Channel zone driver to manage FC zoning in
Cisco SAN fabrics.
Version history:
1.0 - Initial Cisco FC zone driver
"""
VERSION = "1.0.0"
def __init__(self, **kwargs):
super(CiscoFCZoneDriver, self).__init__(**kwargs)
self.configuration = kwargs.get('configuration', None)
if self.configuration:
self.configuration.append_config_values(cisco_opts)
# Adding a hack to handle parameters from super classes
# in case configured with multi backends.
fabric_names = self.configuration.safe_get('fc_fabric_names')
activate = self.configuration.safe_get('cisco_zone_activate')
prefix = self.configuration.safe_get('cisco_zone_name_prefix')
base_san_opts = []
if not fabric_names:
base_san_opts.append(
cfg.StrOpt('fc_fabric_names', default=None,
help='Comma separated list of fibre channel '
'fabric names. This list of names is used to'
' retrieve other SAN credentials for connecting'
' to each SAN fabric'
))
if not activate:
base_san_opts.append(
cfg.BoolOpt('cisco_zone_activate',
default=True,
help='Indicates whether zone should '
'be activated or not'))
if not prefix:
base_san_opts.append(
cfg.StrOpt('cisco_zone_name_prefix',
default="openstack",
help="A prefix to be used when naming zone"))
if len(base_san_opts) > 0:
CONF.register_opts(base_san_opts)
self.configuration.append_config_values(base_san_opts)
fabric_names = [x.strip() for x in self.
configuration.fc_fabric_names.split(',')]
# There can be more than one SAN in the network and we need to
# get credentials for each SAN.
if fabric_names:
self.fabric_configs = fabric_opts.load_fabric_configurations(
fabric_names)
@lockutils.synchronized('cisco', 'fcfabric-', True)
def add_connection(self, fabric, initiator_target_map):
"""Concrete implementation of add_connection.
Based on zoning policy and state of each I-T pair, list of zone
members are created and pushed to the fabric to add zones. The
new zones created or zones updated are activated based on isActivate
flag set in cinder.conf returned by volume driver after attach
operation.
:param fabric: Fabric name from cinder.conf file
:param initiator_target_map: Mapping of initiator to list of targets
"""
LOG.debug("Add connection for Fabric:%s", fabric)
LOG.info(_("CiscoFCZoneDriver - Add connection "
"for I-T map: %s"), initiator_target_map)
fabric_ip = self.fabric_configs[fabric].safe_get(
'cisco_fc_fabric_address')
fabric_user = self.fabric_configs[fabric].safe_get(
'cisco_fc_fabric_user')
fabric_pwd = self.fabric_configs[fabric].safe_get(
'cisco_fc_fabric_password')
fabric_port = self.fabric_configs[fabric].safe_get(
'cisco_fc_fabric_port')
zoning_policy = self.configuration.zoning_policy
zoning_policy_fab = self.fabric_configs[fabric].safe_get(
'cisco_zoning_policy')
if zoning_policy_fab:
zoning_policy = zoning_policy_fab
zoning_vsan = self.fabric_configs[fabric].safe_get('cisco_zoning_vsan')
LOG.info(_("Zoning policy for Fabric %s"), zoning_policy)
statusmap_from_fabric = self.get_zoning_status(
fabric_ip, fabric_user, fabric_pwd, fabric_port, zoning_vsan)
if statusmap_from_fabric.get('session') == 'none':
cfgmap_from_fabric = self.get_active_zone_set(
fabric_ip, fabric_user, fabric_pwd, fabric_port, zoning_vsan)
zone_names = []
if cfgmap_from_fabric.get('zones'):
zone_names = cfgmap_from_fabric['zones'].keys()
# based on zoning policy, create zone member list and
# push changes to fabric.
for initiator_key in initiator_target_map.keys():
zone_map = {}
initiator = initiator_key.lower()
t_list = initiator_target_map[initiator_key]
if zoning_policy == 'initiator-target':
for t in t_list:
target = t.lower()
zone_members = [get_formatted_wwn(initiator),
get_formatted_wwn(target)]
zone_name = (self.
configuration.cisco_zone_name_prefix
+ initiator.replace(':', '')
+ target.replace(':', ''))
if (len(cfgmap_from_fabric) == 0 or (
zone_name not in zone_names)):
zone_map[zone_name] = zone_members
else:
# This is I-T zoning, skip if zone exists.
LOG.info(_("Zone exists in I-T mode. "
"Skipping zone creation %s"),
zone_name)
elif zoning_policy == 'initiator':
zone_members = [get_formatted_wwn(initiator)]
for t in t_list:
target = t.lower()
zone_members.append(get_formatted_wwn(target))
zone_name = self.configuration.cisco_zone_name_prefix \
+ initiator.replace(':', '')
if len(zone_names) > 0 and (zone_name in zone_names):
zone_members = zone_members + filter(
lambda x: x not in zone_members,
cfgmap_from_fabric['zones'][zone_name])
zone_map[zone_name] = zone_members
else:
msg = _("Zoning Policy: %s, not"
" recognized") % zoning_policy
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
LOG.info(_("Zone map to add: %s"), zone_map)
if len(zone_map) > 0:
conn = None
try:
conn = importutils.import_object(
self.configuration.cisco_sb_connector,
ipaddress=fabric_ip,
username=fabric_user,
password=fabric_pwd,
port=fabric_port,
vsan=zoning_vsan)
conn.add_zones(
zone_map, self.configuration.cisco_zone_activate,
zoning_vsan, cfgmap_from_fabric,
statusmap_from_fabric)
conn.cleanup()
except exception.CiscoZoningCliException as cisco_ex:
msg = _("Exception: %s") % six.text_type(cisco_ex)
raise exception.FCZoneDriverException(msg)
except Exception as e:
LOG.error(_("Exception: %s") % six.text_type(e))
msg = (_("Failed to add zoning configuration %s") %
six.text_type(e))
raise exception.FCZoneDriverException(msg)
LOG.debug("Zones added successfully: %s", zone_map)
else:
LOG.debug("Zoning session exists VSAN: %s", zoning_vsan)
@lockutils.synchronized('cisco', 'fcfabric-', True)
def delete_connection(self, fabric, initiator_target_map):
"""Concrete implementation of delete_connection.
Based on zoning policy and state of each I-T pair, list of zones
are created for deletion. The zones are either updated deleted based
on the policy and attach/detach state of each I-T pair.
:param fabric: Fabric name from cinder.conf file
:param initiator_target_map: Mapping of initiator to list of targets
"""
LOG.debug("Delete connection for fabric:%s", fabric)
LOG.info(_("CiscoFCZoneDriver - Delete connection for I-T map: %s"),
initiator_target_map)
fabric_ip = self.fabric_configs[fabric].safe_get(
'cisco_fc_fabric_address')
fabric_user = self.fabric_configs[fabric].safe_get(
'cisco_fc_fabric_user')
fabric_pwd = self.fabric_configs[fabric].safe_get(
'cisco_fc_fabric_password')
fabric_port = self.fabric_configs[fabric].safe_get(
'cisco_fc_fabric_port')
zoning_policy = self.configuration.zoning_policy
zoning_policy_fab = self.fabric_configs[fabric].safe_get(
'cisco_zoning_policy')
if zoning_policy_fab:
zoning_policy = zoning_policy_fab
zoning_vsan = self.fabric_configs[fabric].safe_get('cisco_zoning_vsan')
LOG.info(_("Zoning policy for fabric %s"), zoning_policy)
statusmap_from_fabric = self.get_zoning_status(
fabric_ip, fabric_user, fabric_pwd, fabric_port, zoning_vsan)
if statusmap_from_fabric.get('session') == 'none':
cfgmap_from_fabric = self.get_active_zone_set(
fabric_ip, fabric_user, fabric_pwd, fabric_port, zoning_vsan)
zone_names = []
if cfgmap_from_fabric.get('zones'):
zone_names = cfgmap_from_fabric['zones'].keys()
# Based on zoning policy, get zone member list and push
# changes to fabric. This operation could result in an update
# for zone config with new member list or deleting zones from
# active cfg.
LOG.debug("zone config from Fabric: %s", cfgmap_from_fabric)
for initiator_key in initiator_target_map.keys():
initiator = initiator_key.lower()
formatted_initiator = get_formatted_wwn(initiator)
zone_map = {}
zones_to_delete = []
t_list = initiator_target_map[initiator_key]
if zoning_policy == 'initiator-target':
# In this case, zone needs to be deleted.
for t in t_list:
target = t.lower()
zone_name = (
self.configuration.cisco_zone_name_prefix
+ initiator.replace(':', '')
+ target.replace(':', ''))
LOG.debug("Zone name to del: %s", zone_name)
if (len(zone_names) > 0 and (zone_name in zone_names)):
# delete zone.
LOG.debug("Added zone to delete to list: %s",
zone_name)
zones_to_delete.append(zone_name)
elif zoning_policy == 'initiator':
zone_members = [formatted_initiator]
for t in t_list:
target = t.lower()
zone_members.append(get_formatted_wwn(target))
zone_name = self.configuration.cisco_zone_name_prefix \
+ initiator.replace(':', '')
if (zone_names and (zone_name in zone_names)):
filtered_members = filter(
lambda x: x not in zone_members,
cfgmap_from_fabric['zones'][zone_name])
# The assumption here is that initiator is always
# there in the zone as it is 'initiator' policy.
# We find the filtered list and if it is non-empty,
# add initiator to it and update zone if filtered
# list is empty, we remove that zone.
LOG.debug("Zone delete - I mode: filtered targets:%s",
filtered_members)
if filtered_members:
filtered_members.append(formatted_initiator)
LOG.debug("Filtered zone members to update: %s",
filtered_members)
zone_map[zone_name] = filtered_members
LOG.debug("Filtered zone Map to update: %s",
zone_map)
else:
zones_to_delete.append(zone_name)
else:
LOG.info(_("Zoning Policy: %s, not recognized"),
zoning_policy)
LOG.debug("Final Zone map to update: %s", zone_map)
LOG.debug("Final Zone list to delete: %s", zones_to_delete)
conn = None
try:
conn = importutils.import_object(
self.configuration.cisco_sb_connector,
ipaddress=fabric_ip,
username=fabric_user,
password=fabric_pwd,
port=fabric_port,
vsan=zoning_vsan)
# Update zone membership.
if zone_map:
conn.add_zones(
zone_map, self.configuration.cisco_zone_activate,
zoning_vsan, cfgmap_from_fabric,
statusmap_from_fabric)
# Delete zones ~sk.
if zones_to_delete:
zone_name_string = ''
num_zones = len(zones_to_delete)
for i in range(0, num_zones):
if i == 0:
zone_name_string = ('%s%s' % (
zone_name_string,
zones_to_delete[i]))
else:
zone_name_string = ('%s%s%s' % (
zone_name_string, ';',
zones_to_delete[i]))
conn.delete_zones(zone_name_string,
self.configuration.
cisco_zone_activate,
zoning_vsan, cfgmap_from_fabric,
statusmap_from_fabric)
conn.cleanup()
except Exception as e:
msg = _("Exception: %s") % six.text_type(e)
LOG.error(msg)
msg = _("Failed to update or delete zoning configuration")
raise exception.FCZoneDriverException(msg)
LOG.debug("Zones deleted successfully: %s", zone_map)
else:
LOG.debug("Zoning session exists VSAN: %s", zoning_vsan)
def get_san_context(self, target_wwn_list):
"""Lookup SAN context for visible end devices.
Look up each SAN configured and return a map of SAN (fabric IP) to
list of target WWNs visible to the fabric.
"""
formatted_target_list = []
fabric_map = {}
fabrics = [x.strip() for x in self.
configuration.fc_fabric_names.split(',')]
LOG.debug("Fabric List: %s", fabrics)
LOG.debug("Target wwn List: %s", target_wwn_list)
if len(fabrics) > 0:
for t in target_wwn_list:
formatted_target_list.append(get_formatted_wwn(t.lower()))
LOG.debug("Formatted Target wwn List: %s", formatted_target_list)
for fabric_name in fabrics:
fabric_ip = self.fabric_configs[fabric_name].safe_get(
'cisco_fc_fabric_address')
fabric_user = self.fabric_configs[fabric_name].safe_get(
'cisco_fc_fabric_user')
fabric_pwd = self.fabric_configs[fabric_name].safe_get(
'cisco_fc_fabric_password')
fabric_port = self.fabric_configs[fabric_name].safe_get(
'cisco_fc_fabric_port')
zoning_vsan = self.fabric_configs[fabric_name].safe_get(
'cisco_zoning_vsan')
# Get name server data from fabric and get the targets
# logged in.
nsinfo = None
try:
conn = importutils.import_object(
self.configuration.cisco_sb_connector,
ipaddress=fabric_ip,
username=fabric_user,
password=fabric_pwd, port=fabric_port,
vsan=zoning_vsan)
nsinfo = conn.get_nameserver_info()
LOG.debug("show fcns database info from fabric:%s", nsinfo)
conn.cleanup()
except exception.CiscoZoningCliException as ex:
with excutils.save_and_reraise_exception():
LOG.error(_("Error getting show fcns database "
"info: %s"), six.text_type(ex))
except Exception as e:
msg = (_("Failed to get show fcns database info:%s") %
six.text_type(e))
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
visible_targets = filter(
lambda x: x in formatted_target_list, nsinfo)
if visible_targets:
LOG.info(_("Filtered targets for SAN is: %s"),
{fabric_name: visible_targets})
# getting rid of the ':' before returning
for idx, elem in enumerate(visible_targets):
visible_targets[idx] = six.text_type(
visible_targets[idx]).replace(':', '')
fabric_map[fabric_name] = visible_targets
else:
LOG.debug("No targets are in the fcns info for SAN %s",
fabric_name)
LOG.debug("Return SAN context output:%s", fabric_map)
return fabric_map
def get_active_zone_set(self, fabric_ip,
fabric_user, fabric_pwd, fabric_port,
zoning_vsan):
"""Gets active zoneset config for vsan."""
cfgmap = {}
conn = None
try:
LOG.debug("Southbound connector: %s",
self.configuration.cisco_sb_connector)
conn = importutils.import_object(
self.configuration.cisco_sb_connector,
ipaddress=fabric_ip, username=fabric_user,
password=fabric_pwd, port=fabric_port, vsan=zoning_vsan)
cfgmap = conn.get_active_zone_set()
conn.cleanup()
except Exception as e:
msg = (_("Failed to access active zoning configuration:%s") %
six.text_type(e))
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
LOG.debug("Active zone set from fabric: %s", cfgmap)
return cfgmap
def get_zoning_status(self, fabric_ip, fabric_user, fabric_pwd,
fabric_port, zoning_vsan):
"""Gets zoneset status and mode."""
statusmap = {}
conn = None
try:
LOG.debug("Southbound connector: %s",
self.configuration.cisco_sb_connector)
conn = importutils.import_object(
self.configuration.cisco_sb_connector,
ipaddress=fabric_ip, username=fabric_user,
password=fabric_pwd, port=fabric_port, vsan=zoning_vsan)
statusmap = conn.get_zoning_status()
conn.cleanup()
except Exception as e:
msg = (_("Failed to access zoneset status:%s") %
six.text_type(e))
LOG.error(msg)
raise exception.FCZoneDriverException(msg)
LOG.debug("Zoneset status from fabric: %s", statusmap)
return statusmap

View File

@ -0,0 +1,32 @@
# (c) Copyright 2014 Cisco Systems Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""
Common constants used by Cisco FC Zone Driver.
"""
ACTIVE_ZONE_CONFIG = 'active_zone_config'
CFG_ZONESET = 'zoneset'
CFG_ZONE = 'zone'
CFG_ZONE_MEMBER = 'pwwn'
CFG_ZONES = 'zones'
"""
CLI Commands for FC zoning operations.
"""
GET_ACTIVE_ZONE_CFG = 'show zoneset active vsan '
FCNS_SHOW = 'show fcns database vsan '
GET_ZONE_STATUS = 'show zone status vsan '

View File

@ -61,6 +61,15 @@ def create_lookup_service():
return None
def get_formatted_wwn(wwn_str):
"""Utility API that formats WWN to insert ':'."""
if (len(wwn_str) != 16):
return wwn_str.lower()
else:
return (':'.join([wwn_str[i:i + 2]
for i in range(0, len(wwn_str), 2)])).lower()
def AddFCZone(initialize_connection):
"""Decorator to add a FC Zone."""
def decorator(self, *args, **kwargs):

View File

@ -2224,6 +2224,37 @@
#principal_switch_wwn=<None>
[CISCO_FABRIC_EXAMPLE]
#
# Options defined in cinder.zonemanager.drivers.cisco.cisco_fabric_opts
#
# Management IP of fabric (string value)
#cisco_fc_fabric_address=
# Fabric user ID (string value)
#cisco_fc_fabric_user=
# Password for user (string value)
#cisco_fc_fabric_password=
# Connecting port (integer value)
#cisco_fc_fabric_port=22
# overridden zoning policy (string value)
#cisco_zoning_policy=initiator-target
# overridden zoning activation state (boolean value)
#cisco_zone_activate=true
# overridden zone name prefix (string value)
#cisco_zone_name_prefix=<None>
# VSAN of the Fabric (string value)
#cisco_zoning_vsan=<None>
[database]
#
@ -2351,6 +2382,14 @@
#brcd_sb_connector=cinder.zonemanager.drivers.brocade.brcd_fc_zone_client_cli.BrcdFCZoneClientCLI
#
# Options defined in cinder.zonemanager.drivers.cisco.cisco_fc_zone_driver
#
# Southbound connector for zoning operation (string value)
#cisco_sb_connector=cinder.zonemanager.drivers.cisco.cisco_fc_zone_client_cli.CiscoFCZoneClientCLI
#
# Options defined in cinder.zonemanager.fc_zone_manager
#