cinder/cinder/zonemanager/drivers/brocade/brcd_fc_zone_client_cli.py

545 lines
22 KiB
Python

# (c) Copyright 2014 Brocade Communications Systems Inc.
# All Rights Reserved.
#
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""
Script to push the zone configuration to brocade SAN switches.
"""
import random
import re
from eventlet import greenthread
from cinder import exception
from cinder.openstack.common import excutils
from cinder.openstack.common import log as logging
from cinder.openstack.common import processutils
from cinder import utils
import cinder.zonemanager.drivers.brocade.fc_zone_constants as ZoneConstant
LOG = logging.getLogger(__name__)
class BrcdFCZoneClientCLI(object):
switch_ip = None
switch_port = '22'
switch_user = 'admin'
switch_pwd = 'none'
patrn = re.compile('[;\s]+')
def __init__(self, ipaddress, username, password, port):
"""initializing the client."""
self.switch_ip = ipaddress
self.switch_port = port
self.switch_user = username
self.switch_pwd = password
self.sshpool = None
def get_active_zone_set(self):
"""Return the active zone configuration.
Return active zoneset from fabric. When none of the configurations
are active then it will return empty map.
:returns: Map -- active zone set map in the following format
{
'zones':
{'openstack50060b0000c26604201900051ee8e329':
['50060b0000c26604', '201900051ee8e329']
},
'active_zone_config': 'OpenStack_Cfg'
}
"""
zone_set = {}
zone = {}
zone_member = None
zone_name = None
switch_data = None
zone_set_name = None
try:
switch_data = self._get_switch_info(
[ZoneConstant.GET_ACTIVE_ZONE_CFG])
except exception.BrocadeZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed getting active zone set "
"from fabric %s"), self.switch_ip)
try:
for line in switch_data:
line_split = re.split('\\t', line)
if len(line_split) > 2:
line_split = [x.replace(
'\n', '') for x in line_split]
line_split = [x.replace(
' ',
'') for x in line_split]
if ZoneConstant.CFG_ZONESET in line_split:
zone_set_name = line_split[1]
continue
if line_split[1]:
zone_name = line_split[1]
zone[zone_name] = list()
if line_split[2]:
zone_member = line_split[2]
if zone_member:
zone_member_list = zone.get(zone_name)
zone_member_list.append(zone_member)
zone_set[ZoneConstant.CFG_ZONES] = zone
zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG] = zone_set_name
except Exception as ex:
# Incase of parsing error here, it should be malformed cli output.
msg = _("Malformed zone configuration: (switch=%(switch)s "
"zone_config=%(zone_config)s)."
) % {'switch': self.switch_ip,
'zone_config': switch_data}
LOG.error(msg)
LOG.exception(ex)
raise exception.FCZoneDriverException(reason=msg)
switch_data = None
return zone_set
def add_zones(self, zones, activate):
"""Add zone configuration.
This method will add the zone configuration passed by user.
input params:
zones - zone names mapped to members.
zone members are colon separated but case-insensitive
{ zonename1:[zonememeber1,zonemember2,...],
zonename2:[zonemember1, zonemember2,...]...}
e.g: {'openstack50060b0000c26604201900051ee8e329':
['50:06:0b:00:00:c2:66:04', '20:19:00:05:1e:e8:e3:29']
}
activate - True/False
"""
LOG.debug(_("Add Zones - Zones passed: %s"), zones)
cfg_name = None
iterator_count = 0
zone_with_sep = ''
active_zone_set = self.get_active_zone_set()
LOG.debug(_("Active zone set:%s"), active_zone_set)
zone_list = active_zone_set[ZoneConstant.CFG_ZONES]
LOG.debug(_("zone list:%s"), zone_list)
for zone in zones.keys():
# if zone exists, its an update. Delete & insert
# TODO(skolathur): This can be optimized to an update call later
LOG.debug("Update call")
if (zone in zone_list):
try:
self.delete_zones(zone, activate)
except exception.BrocadeZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Deleting zone failed %s"), zone)
LOG.debug(_("Deleted Zone before insert : %s"), zone)
zone_members_with_sep = ';'.join(str(member) for
member in zones[zone])
LOG.debug(_("Forming command for add zone"))
cmd = 'zonecreate "%(zone)s", "%(zone_members_with_sep)s"' % {
'zone': zone,
'zone_members_with_sep': zone_members_with_sep}
LOG.debug(_("Adding zone, cmd to run %s"), cmd)
self.apply_zone_change(cmd.split())
LOG.debug(_("Created zones on the switch"))
if(iterator_count > 0):
zone_with_sep += ';'
iterator_count += 1
zone_with_sep += zone
try:
cfg_name = active_zone_set[ZoneConstant.ACTIVE_ZONE_CONFIG]
cmd = None
if not cfg_name:
cfg_name = ZoneConstant.OPENSTACK_CFG_NAME
cmd = 'cfgcreate "%(zoneset)s", "%(zones)s"' \
% {'zoneset': cfg_name, 'zones': zone_with_sep}
else:
cmd = 'cfgadd "%(zoneset)s", "%(zones)s"' \
% {'zoneset': cfg_name, 'zones': zone_with_sep}
LOG.debug(_("New zone %s"), cmd)
self.apply_zone_change(cmd.split())
self._cfg_save()
if activate:
self.activate_zoneset(cfg_name)
except Exception as e:
self._cfg_trans_abort()
msg = _("Creating and activating zone set failed: "
"(Zone set=%(cfg_name)s error=%(err)s)."
) % {'cfg_name': cfg_name, 'err': e}
LOG.error(msg)
raise exception.BrocadeZoningCliException(reason=msg)
def activate_zoneset(self, cfgname):
"""Method to Activate the zone config. Param cfgname - ZonesetName."""
cmd_list = [ZoneConstant.ACTIVATE_ZONESET, cfgname]
return self._ssh_execute(cmd_list, True, 1)
def deactivate_zoneset(self):
"""Method to deActivate the zone config."""
return self._ssh_execute([ZoneConstant.DEACTIVATE_ZONESET], True, 1)
def delete_zones(self, zone_names, activate):
"""Delete zones from fabric.
Method to delete the active zone config zones
params zone_names: zoneNames separated by semicolon
params activate: True/False
"""
active_zoneset_name = None
active_zone_set = None
zone_list = []
active_zone_set = self.get_active_zone_set()
active_zoneset_name = active_zone_set[
ZoneConstant.ACTIVE_ZONE_CONFIG]
zone_list = active_zone_set[ZoneConstant.CFG_ZONES]
zones = self.patrn.split(''.join(zone_names))
cmd = None
try:
if len(zones) == len(zone_list):
self.deactivate_zoneset()
cmd = 'cfgdelete "%(active_zoneset_name)s"' \
% {'active_zoneset_name': active_zoneset_name}
# Active zoneset is being deleted, hence reset is_active
activate = False
else:
cmd = 'cfgremove "%(active_zoneset_name)s", "%(zone_names)s"' \
% {'active_zoneset_name': active_zoneset_name,
'zone_names': zone_names
}
LOG.debug(_("Delete zones: Config cmd to run:%s"), cmd)
self.apply_zone_change(cmd.split())
for zone in zones:
self._zone_delete(zone)
self._cfg_save()
if activate:
self.activate_zoneset(active_zoneset_name)
except Exception as e:
msg = _("Deleting zones failed: (command=%(cmd)s error=%(err)s)."
) % {'cmd': cmd, 'err': e}
LOG.error(msg)
self._cfg_trans_abort()
raise exception.BrocadeZoningCliException(reason=msg)
def get_nameserver_info(self):
"""Get name server data from fabric.
This method will return the connected node port wwn list(local
and remote) for the given switch fabric
"""
cli_output = None
return_list = []
try:
cli_output = self._get_switch_info([ZoneConstant.NS_SHOW])
except exception.BrocadeZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed collecting nsshow "
"info for fabric %s"), self.switch_ip)
if (cli_output):
return_list = self._parse_ns_output(cli_output)
try:
cli_output = self._get_switch_info([ZoneConstant.NS_CAM_SHOW])
except exception.BrocadeZoningCliException:
with excutils.save_and_reraise_exception():
LOG.error(_("Failed collecting nscamshow "
"info for fabric %s"), self.switch_ip)
if (cli_output):
return_list.extend(self._parse_ns_output(cli_output))
cli_output = None
return return_list
def _cfg_save(self):
self._ssh_execute([ZoneConstant.CFG_SAVE], True, 1)
def _zone_delete(self, zone_name):
cmd = 'zonedelete "%(zone_name)s"' % {'zone_name': zone_name}
self.apply_zone_change(cmd.split())
def _cfg_trans_abort(self):
is_abortable = self._is_trans_abortable()
if(is_abortable):
self.apply_zone_change([ZoneConstant.CFG_ZONE_TRANS_ABORT])
def _is_trans_abortable(self):
is_abortable = False
stdout, stderr = None, None
stdout, stderr = self._run_ssh(
[ZoneConstant.CFG_SHOW_TRANS], True, 1)
output = stdout.splitlines()
is_abortable = False
for line in output:
if(ZoneConstant.TRANS_ABORTABLE in line):
is_abortable = True
break
if stderr:
msg = _("Error while checking transaction status: %s") % stderr
raise exception.BrocadeZoningCliException(reason=msg)
else:
return is_abortable
def apply_zone_change(self, cmd_list):
"""Execute zoning cli with no status update.
Executes CLI commands such as addZone where status return is
not expected.
"""
stdout, stderr = None, None
LOG.debug(_("Executing command via ssh: %s"), cmd_list)
stdout, stderr = self._run_ssh(cmd_list, True, 1)
# no output expected, so output means there is an error
if stdout:
msg = _("Error while running zoning CLI: (command=%(cmd)s "
"error=%(err)s).") % {'cmd': cmd_list, 'err': stdout}
LOG.error(msg)
self._cfg_trans_abort()
raise exception.BrocadeZoningCliException(reason=msg)
def is_supported_firmware(self):
"""Check firmware version is v6.4 or higher.
This API checks if the firmware version per the plug-in support level.
This only checks major and minor version.
"""
cmd = ['version']
firmware = 0
try:
stdout, stderr = self._execute_shell_cmd(cmd)
if (stdout):
for line in stdout:
if 'Fabric OS: v' in line:
LOG.debug(_("Firmware version string:%s"), line)
ver = line.split('Fabric OS: v')[1].split('.')
if (ver):
firmware = int(ver[0] + ver[1])
return firmware > 63
else:
LOG.error(_("No CLI output for firmware version check"))
return False
except processutils.ProcessExecutionError as e:
msg = _("Error while getting data via ssh: (command=%(cmd)s "
"error=%(err)s).") % {'cmd': cmd, 'err': e}
LOG.error(msg)
raise exception.BrocadeZoningCliException(reason=msg)
def _get_switch_info(self, cmd_list):
stdout, stderr, sw_data = None, None, None
try:
stdout, stderr = self._run_ssh(cmd_list, True, 1)
if (stdout):
sw_data = stdout.splitlines()
return sw_data
except processutils.ProcessExecutionError as e:
msg = _("Error while getting data via ssh: (command=%(cmd)s "
"error=%(err)s).") % {'cmd': cmd_list,
'err': e}
LOG.error(msg)
raise exception.BrocadeZoningCliException(reason=msg)
def _parse_ns_output(self, switch_data):
"""Parses name server data.
Parses nameserver raw data and adds the device port wwns to the list
:returns: List -- list of device port wwn from ns info
"""
return_list = []
for line in switch_data:
if not(" NL " in line or " N " in line):
continue
linesplit = line.split(';')
if len(linesplit) > 2:
node_port_wwn = linesplit[2]
return_list.append(node_port_wwn)
else:
msg = _("Malformed nameserver string: %s") % line
LOG.error(msg)
raise exception.InvalidParameterValue(err=msg)
return return_list
def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1):
# TODO(skolathur): Need to implement ssh_injection check
# currently, the check will fail for zonecreate command
# as zone members are separated by ';'which is a danger char
command = ' '. join(cmd_list)
if not self.sshpool:
self.sshpool = utils.SSHPool(self.switch_ip,
self.switch_port,
None,
self.switch_user,
self.switch_pwd,
min_size=1,
max_size=5)
last_exception = None
try:
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
return processutils.ssh_execute(
ssh,
command,
check_exit_code=check_exit_code)
except Exception as e:
LOG.error(e)
last_exception = e
greenthread.sleep(random.randint(20, 500) / 100.0)
try:
raise processutils.ProcessExecutionError(
exit_code=last_exception.exit_code,
stdout=last_exception.stdout,
stderr=last_exception.stderr,
cmd=last_exception.cmd)
except AttributeError:
raise processutils.ProcessExecutionError(
exit_code=-1,
stdout="",
stderr="Error running SSH command",
cmd=command)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_("Error running SSH command: %s") % command)
def _ssh_execute(self, cmd_list, check_exit_code=True, attempts=1):
"""Execute cli with status update.
Executes CLI commands such as cfgsave where status return is expected.
"""
utils.check_ssh_injection(cmd_list)
command = ' '. join(cmd_list)
if not self.sshpool:
self.sshpool = utils.SSHPool(self.switch_ip,
self.switch_port,
None,
self.switch_user,
self.switch_pwd,
min_size=1,
max_size=5)
stdin, stdout, stderr = None, None, None
LOG.debug(_("Executing command via ssh: %s") % command)
last_exception = None
try:
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
stdin, stdout, stderr = ssh.exec_command(command)
greenthread.sleep(random.randint(20, 500) / 100.0)
stdin.write("%s\n" % ZoneConstant.YES)
channel = stdout.channel
exit_status = channel.recv_exit_status()
LOG.debug(_("Exit Status from ssh:%s"), exit_status)
# exit_status == -1 if no exit code was returned
if exit_status != -1:
LOG.debug(_('Result was %s') % exit_status)
if check_exit_code and exit_status != 0:
raise processutils.ProcessExecutionError(
exit_code=exit_status,
stdout=stdout,
stderr=stderr,
cmd=command)
else:
return True
else:
return True
except Exception as e:
LOG.error(e)
last_exception = e
greenthread.sleep(random.randint(20, 500) / 100.0)
LOG.debug(_("Handling error case after "
"SSH:%s"), last_exception)
try:
raise processutils.ProcessExecutionError(
exit_code=last_exception.exit_code,
stdout=last_exception.stdout,
stderr=last_exception.stderr,
cmd=last_exception.cmd)
except AttributeError:
raise processutils.ProcessExecutionError(
exit_code=-1,
stdout="",
stderr="Error running SSH command",
cmd=command)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(_("Error executing command via ssh: %s"), e)
finally:
if stdin:
stdin.flush()
stdin.close()
if stdout:
stdout.close()
if stderr:
stderr.close()
def _execute_shell_cmd(self, cmd):
"""Run command over shell for older firmware versions.
We invoke shell and issue the command and return the output.
This is primarily used for issuing read commands when we are not sure
if the firmware supports exec_command.
"""
utils.check_ssh_injection(cmd)
command = ' '. join(cmd)
stdout, stderr = None, None
if not self.sshpool:
self.sshpool = utils.SSHPool(self.switch_ip,
self.switch_port,
None,
self.switch_user,
self.switch_pwd,
min_size=1,
max_size=5)
with self.sshpool.item() as ssh:
LOG.debug('Running cmd (SSH): %s' % command)
channel = ssh.invoke_shell()
stdin_stream = channel.makefile('wb')
stdout_stream = channel.makefile('rb')
stderr_stream = channel.makefile('rb')
stdin_stream.write('''%s
exit
''' % command)
stdin_stream.flush()
stdout = stdout_stream.readlines()
stderr = stderr_stream.readlines()
stdin_stream.close()
stdout_stream.close()
stderr_stream.close()
exit_status = channel.recv_exit_status()
# exit_status == -1 if no exit code was returned
if exit_status != -1:
LOG.debug('Result was %s' % exit_status)
if exit_status != 0:
msg = "command %s failed" % command
LOG.debug(msg)
raise processutils.ProcessExecutionError(
exit_code=exit_status,
stdout=stdout,
stderr=stderr,
cmd=command)
try:
channel.close()
except Exception as e:
LOG.exception(e)
LOG.debug("_execute_cmd: stdout to return:%s" % stdout)
LOG.debug("_execute_cmd: stderr to return:%s" % stderr)
return (stdout, stderr)
def cleanup(self):
self.sshpool = None