# !usr/bin/env python
"""
initiators
----------
These are classes for interacting with Solaris hosts.
Basic Usage::
from otto.initiators.solaris import SolarisSsh
s = SolarisSsh(uname, host, passwd, prompt=None)
s.connect()
logger.info(s.release)
s.disconnect()
"""
import re
import logging
import time
import os
from collections import defaultdict
from otto.lib.otypes import AoEAddress
from otto.utils import now
from otto.lib.otypes import InitiatorError, ReturnCode, Namespace
from otto.lib.pexpect import TIMEOUT
from otto.initiators.ethdrv import Ethdrv
from otto.connections.ssh import Client
from otto.lib.contextmanagers import ignored
from otto.lib.decorators import wait_until
instance = os.environ.get('instance') or ''
logger = logging.getLogger('otto' + instance + '.initiators')
logger.addHandler(logging.NullHandler())
class Initiator(object):
def __init__(self, coraid_module):
self.coraid_module = coraid_module
self._aoeversion = None
def aoediscover(self):
"""
Call the driver's discover command. Returns ReturnCode object
"""
cmd = 'echo 11 discover > /dev/ethdrv/ctl'
return self.run_and_check(cmd)
def aoeflush(self, aflag=True):
"""
Call the driver's flush command. Return a ReturnCode object.
"""
return self.run_and_check('ethdrvadm flush %s' % ('', '-a')[aflag])
@property
def aoestat(self):
"""
Returns a dictionary of ethdrvadm list-devices -a in the following format::
{'183.91': {'claim': None, 'target': '183.91', 'ifs': [1], 'iounit': None, 'state': None,
'file': 'sd39', 'device': 'sd39', 'path': None, 'port': [1], 'size': '8.000GB',
'paths': defaultdict(<function <lambda> at 0x10e432398>, {1: {'port': 1,
'address': ['002590c23e8a']}}),
'targpath': defaultdict(<function <lambda> at 0x10e432398>, {1: {'port': 1,
'address': ['002590c23e8a']}}),
If port is N/A, port is an empty list and targpath/paths are empty dicts.
"""
aoedd = defaultdict(
lambda: {'file': None, 'device': None, 'path': None, 'port': None, 'ifs': None, 'target': None,
'size': None, 'iounit': None, 'state': None, 'claim': None, 'paths': None,
'targpath': defaultdict(lambda: {'address': None, 'port': None})})
out = self.run_and_check('ethdrvadm list-devices -a')
m = out.message.strip().splitlines() # [header1, header2, lines, of, devices]
if not m:
return aoedd
for l in m[2:]:
if not l.startswith(' '):
w = l.split() # ['device', 'target', 'size', 'port']
target = w[1]
if w[3].find('N/A') is -1:
w[3] = [int(n) for n in w[3].split(',')]
else:
w[3] = list()
aoedd[target]['file'] = w[0]
aoedd[target]['device'] = aoedd[target]['file'] # deprecated
aoedd[target]['target'] = target
aoedd[target]['size'] = w[2]
aoedd[target]['port'] = w[3]
aoedd[target]['ifs'] = aoedd[target]['port'] # deprecated
aoedd[target] = Namespace(aoedd[target])
else:
w = l.split() # ['port', 'addr0,addr1']
w[0] = int(w[0])
w[1] = w[1].split(',')
aoedd[target]['targpath'][w[0]]['port'] = w[0]
aoedd[target]['targpath'][w[0]]['address'] = w[1]
aoedd[target]['targpath'][w[0]] = Namespace(aoedd[target]['targpath'][w[0]])
aoedd[target]['paths'] = aoedd[target]['targpath'] # deprecated
return aoedd
@property
def aoeversion(self):
"""
Returns the driver version as a dict::
{ 'major': 6,
'minor': 0,
'revision' : 1,
'release': 'R5'
}
"""
out = self.run_and_check('ethdrvadm version')
l = out.message.splitlines()[0].split('.')
l[2:3] = l[2].split('-')
head = ['major', 'minor', 'revision', 'release']
self._aoeversion = dict(zip(head, l))
return self._aoeversion
def df(self, mount_point):
"""
Returns the amount of disk space available a given file system containing each
file name argument. Disk space is returned in human readable format (--human-readable).
"""
if not os.path.exists(mount_point):
raise Exception("Mount point %s does not exist" % mount_point)
cmd = "df -h %s" % mount_point
ret = self.run_and_check(cmd)
output = str(ret)
logger.debug(output)
header = re.compile(r"Filesystem[ \t]*Size[ \t]*Used[ \t]*Available[ \t]*Capacity[ \t]*Mounted on")
lines = output.splitlines()
info = dict()
if not header.search(lines[0]):
print("output not recognized")
else:
if len(lines) > 3:
line = " ".join(lines[1:len(lines)])
else:
line = lines[1]
[filesystem, size, used, avail, percent_used, mounted_on] = line.split()
percent_used = percent_used.replace('%', '')
info['filesystem'] = filesystem
info['size'] = size
info['used'] = used
info['avail'] = avail
info['percent_used'] = int(percent_used)
info['mounted_on'] = mounted_on
return info
def run_and_check(self, cmd, expectation=True):
if True:
raise NotImplemented
return ReturnCode(False)
class ZFSSystem(object):
"""
Class for operating on zpools. This is used with multiple inheritance for creating Solaris objects.
"""
def zpool_create(self, pname, targets, ptype='', num_devices=1, spares=None, timeout=600, expectation=True):
"""
This method will create a zpool of name pname; working on support for multiple vdevs
:param pname: zpool name
:param targets: a list of SRX targets in the form shelf.lun
:param ptype: a type for the zpool to create, like raidz2, mirror, etc
:param num_devices: number of devices to use per vdev
:param spares: is a list of targets that will be used as spare for the zpool
:param expectation: If the caller cares about failure and the command fails we raise a generic exception.
Returns a ReturnCode object
"""
pools_list = self.zpool_list # get the list of current existing pools
addlist = targets
if pools_list.get(pname): # pool already exist and we need to add devices
while addlist:
r = self.zpool_add(pname, addlist[:num_devices], ptype, expectation)
if r:
addlist = addlist[num_devices:]
elif not expectation:
return ReturnCode(False, 'An error was detected while creating pool: %s' % r.message)
else: # pool does not exist, we need to create it first.
r = self._zpool_create(pname, addlist[:num_devices], ptype, spares)
if expectation and not r:
raise InitiatorError("%s" % r)
addlist = addlist[num_devices:]
while addlist:
r = self.zpool_add(pname, addlist[:num_devices], ptype, expectation)
if r:
addlist = addlist[num_devices:]
elif not expectation:
return ReturnCode(False, 'An error was detected while creating pool:\n %s' % r.message)
else:
raise InitiatorError("%s" % r)
start = now()
deadline = start + timeout
# This should probably become otto.lib.solaris.target_in_zpool
waitlist = [self._target2device(target) for target in targets]
while waitlist:
for pool in self.zpool_status():
if pool['pool'] == pname:
for target in waitlist:
if target in pool['config'].keys():
waitlist.remove(target)
if now() > deadline:
raise InitiatorError("missed deadline: %s timed out %ssec waiting on:\n%s" % (deadline,
now() - start,
waitlist))
for pool in self.zpool_status():
if pool['pool'] == pname:
return ReturnCode(True, message=pool)
return ReturnCode(False, "Couldn't find pool after creation")
def _target2device(self, t): # TODO: Replace with lib.solaris.targ2disk
"""
This method will try to identify a device name for a particular target.
"""
device = self.run_and_check('ls /dev/rdsk/c*t%sd%s*' % tuple(t.split('.')))
rregex = re.match('/dev/rdsk/c(\d+)t%sd%s' % tuple(t.split('.')), device.message)
if rregex:
return 'c%s' % rregex.group(1) + 't%sd%s' % tuple(t.split('.'))
else:
return None
def zpool_root(self):
"""
Return the root zpool
"""
result = self.run_and_check('zfs list')
if result:
for line in result.message.splitlines():
if line:
ls = line.split()
if ls[4] == '/':
r = ls[0].split('/')
return r[0]
logger.warn("Unable to find root pool: %s" % result.message)
return ''
def zpool_add(self, pname, targets, ptype='', expectation=True):
"""
Take a list of targets (shelf.lun), find the device for them and add it to an existing zpool.
"""
if type(targets) == list:
devices = [self._target2device(target) for target in targets] # target is in the form shelf.lun
if len(devices) > 0:
cmd = 'zpool add -f %s %s' % (pname, ptype)
for d in devices:
if d: # Skip devices not found, will be reported as None.
cmd += ' %s' % d
return self.run_and_check(cmd, False)
else:
return ReturnCode(False, 'Devices were not found')
elif type(targets) == str:
device = self._target2device(targets)
if device:
cmd = 'zpool add -f %s %s %s' % (pname, ptype, device)
return self.run_and_check(cmd, expectation)
else:
return ReturnCode(False, 'Device for target %s not found' % targets)
def _zpool_create(self, pname, targets, ptype='', spares=None):
"""
This gets called from abstraction zpool_create and does zpool creation.
"""
if type(targets) == list:
devices = [self._target2device(target) for target in targets]
spare_devices = []
if spares:
spare_devices = [self._target2device(spare) for spare in spares]
if len(devices) > 0:
cmd = 'zpool create -f %s %s' % (pname, ptype)
for d in devices:
if d:
cmd += ' %s' % d
if spares:
cmd += ' spare'
for s in spare_devices:
if s:
cmd += ' %s' % s
return self.run_and_check(cmd, False)
# it could be a question being asked during the creation.
else:
return ReturnCode(False, 'Devices not found at initiator')
elif type(targets) == str:
device = self._target2device(targets)
spare_devices = []
if spares:
spare_devices = [self._target2device(spare) for spare in spares]
if device:
cmd = 'zpool create -f %s %s %s' % (pname, ptype, device)
if spares:
cmd += ' spare'
for s in spare_devices:
if s:
cmd += ' %s' % s
return self.run_and_check(cmd, False) # FIXME, same as above run command.
else:
return ReturnCode(False, 'Device for target %s not found' % targets)
def zpool_destroy(self, pname, expectation=True, force=False):
"""
Remove an existing zpool from the system.
"""
if force:
cmd = 'zpool destroy -f %s' % pname
else:
cmd = 'zpool destroy %s' % pname
return self.run_and_check(cmd, expectation)
@property
def zpools(self):
"""
Return a dictionary with some useful information taken from zpool list -H command::
{'rpool': {'alloc': '6.72G',
'altroot': '-',
'cap': '53%',
'dedup': '1.00x',
'free': '5.90G',
'health': 'ONLINE',
'size': '12.6G'}}
"""
cmd = 'zpool list -H' # -H no headers and separate fields by a single tab instead of variable space.
rdict = dict()
columns = ['size', 'alloc', 'free', 'cap', 'dedup', 'health', 'altroot'] # zpool @ SunOS 5.11
result = self.run_and_check(cmd)
if result:
for line in result.message.splitlines():
if line:
ls = line.split()
if len(ls[1:]) == len(columns):
rdict[ls[0]] = dict(zip(columns, ls[1:]))
return rdict
@property
def zpool_list(self):
return self.zpools
@property
def zfs_list(self):
"""
Return a dictionary from zfs list -H command::
coraid@solaris-client-4:~$ zfs list -H
rpool 4.69G 51.9G 4.61M /rpool
rpool/ROOT 2.62G 51.9G 31K legacy
rpool/ROOT/solaris 2.62G 51.9G 2.49G /
Returns:
{'rpool': {'avail': '51.9G',
'mountpoint': '/rpool',
'name': 'rpool',
'refer': '4.61M',
'used': '4.69G'},
'rpool/ROOT': {'avail': '51.9G',
'mountpoint': 'legacy',
'name': 'rpool/ROOT',
'refer': '31K',
'used': '2.63G'},
'rpool/ROOT/solaris': {'avail': '51.9G',
'mountpoint': '/',
'name': 'rpool/ROOT/solaris',
'refer': '2.49G',
'used': '2.63G'}}
:rtype: dict
"""
cmd = 'zfs list -H' # -H Scripted mode. Do not display headers & separate fields
# by a single tab instead of arbitrary space.
rdict = dict()
columns = ['name', 'used', 'avail', 'refer', 'mountpoint'] # zfs @ SunOS 5.11
result = self.run_and_check(cmd)
if result:
for line in re.split('\r+\n', result.message.strip()):
l = line.split()
if len(l) == len(columns):
rdict[l[0]] = dict(zip(columns, l))
return rdict
@property
def zfs_get_share(self):
"""
a dictionary with some useful information taken from zfs get share command.
:rtype: dict
For example::
coraid@solaris-client-4:~$ zfs get share
NAME PROPERTY VALUE SOURCE
bpool3/users/cifs1 share name=cifs1,path=/bpool3/users/cifs1,prot=smb local
bpool3/users/cifs2 share name=cifs2,path=/bpool3/users/cifs2,prot=smb local
bpool3/users/nfs1 share name=nfs1,path=/bpool3/users/nfs1,prot=nfs local
bpool3/users/nfs2 share name=nfs2,path=/bpool3/users/nfs2,prot=nfs local
Returns::
{'rpool': {'avail': '51.9G',
'mountpoint': '/rpool',
'name': 'rpool',
'refer': '4.61M',
'used': '4.69G'},
'rpool/ROOT': {'avail': '51.9G',
'mountpoint': 'legacy',
'name': 'rpool/ROOT',
'refer': '31K',
'used': '2.63G'},
'rpool/ROOT/solaris': {'avail': '51.9G',
'mountpoint': '/',
'name': 'rpool/ROOT/solaris',
'refer': '2.49G',
'used': '2.63G'}}
:rtype: dict
"""
cmd = 'zfs get share'
rdict = dict()
columns = ['name', 'property', 'value', 'source'] # zfs @ SunOS 5.11
result = self.run_and_check(cmd)
if result:
for line in re.split('\r+\n', result.message.strip()):
if line.startswith('NAME'):
continue
l = line.split()
if len(l) == len(columns):
rdict[l[0]] = dict(zip(columns, l))
return rdict
def zpool_wipe(self, expectation=True, force=False):
"""
Return list of the zpool existing in the system and will try to remove them.
"""
rpool = self.zpool_root()
plist = self.zpool_list
for pool in plist:
if pool != rpool:
self.zpool_destroy(pool, expectation, force)
time.sleep(2)
def zpool_import(self, pname):
"""
Import the string pname
"""
cmd = 'zpool import %s' % pname
return self.run_and_check(cmd, False)
def zpool_export(self, pname):
"""
Export the string pname
"""
cmd = 'zpool export %s' % pname
return self.run_and_check(cmd, False)
def zpool_status(self, expectation=True):
pools = list()
for pool in self.zpools.keys():
cmd = "zpool status -v %s" % pool
r = self.run_and_check(cmd)
if r:
pools.append(self.parse_status(r.message))
elif expectation:
raise InitiatorError("zpool statius failed:\n%s" % r.message)
return pools
@staticmethod
def parse_status(status):
pool = dict()
pool['config'] = dict()
try:
summary, config = re.split("NAME\s+STATE\s+READ\s+WRITE\s+CKSUM", status)
except ValueError:
raise InitiatorError("Couldn't find regions in status output:\n%s" % status)
for line in summary.strip().splitlines():
k, v = line.split(':', 1)
if k == 'config':
continue
pool[k.strip()] = v.strip()
for line in config.strip().splitlines():
if re.match(r"^\s", line):
name, state, read, write, cksum = line.split()[:5]
pool['config'][name] = {'name': name, 'state': state, 'read': read, 'write': write, 'cksum': cksum}
detail = line.split()[5:]
if detail:
pool['config']['detail'] = detail
return pool
def run_and_check(self, cmd, expectation=True):
if True:
raise NotImplemented
return ReturnCode(False)
class RSFSystem(object):
@property
def rsfcli_status(self):
"""
Returns a dictionary containing information about the rsf configuration::
{'cluster': {'CRC': '0x5d68', 'name': 'bbox-cluster-1'},
'errors': 0,
'hearbeat': {'0': {'destination': 'street [10.175.50.76]',
'index': '0',
'last_index': '154376',
'last_timestamp': 'Thu Jan 23 11:01:33',
'source': 'wells',
'state': 'Up',
'type': 'net'},
'1': {'destination': 'street',
'disc': 'c0t5000C500572F501Fd0s0',
'index': '1',
'last_index': '154376',
'last_timestamp': 'Thu Jan 23 11:01:33',
'readsector': '518',
'source': 'wells',
'state': 'Up',
'type': 'disc',
'writesector': '512'}},
'heartbeats': {'configured': '6', 'down': '0', 'up': '6'},
'hosts': {'street': {'built_on': '12-Nov-2013-11:39',
'hostname': 'street',
'ip': '10.175.50.76',
'service_startups': 'enabled',
'state': 'UP'},
'wells': {'built_on': '12-Nov-2013-11:39',
'hostname': 'wells',
'ip': '10.175.50.74',
'service_startups': 'enabled',
'state': 'UP'}},
'nodes': {'configured': '2', 'online': '2'},
'service': {'0': {'description': 'Coraid bpool3 ZFS service',
'hosts': {'street': {'mode': 'automatic',
'name': 'street',
'state': 'unblocked',
'status': 'stopped'},
'wells': {'mode': 'automatic',
'name': 'wells',
'state': 'unblocked',
'status': 'running'}},
'index': '0',
'ip': 'bbox-vip-1',
'name': 'bpool3'}},
'services': {'running': '1', 'stopped': '1'}}
"""
rdict = dict()
cmd = 'rsfcli status'
result = self.run_and_check(cmd)
if result:
if not re.search('rsfcli: command not found', result.message):
rdict['hosts'] = dict()
rdict['service'] = dict()
rdict['services'] = dict()
for line in result.message.splitlines():
if line:
# Contacted localhost in cluster "bbox-cluster-1", CRC = 0x5d68
m = re.search(
'Contacted\s+localhost\s+in\s+\cluster\s+\"(?P<name>[^\"]+)\",\s+CRC\s+=\s+(?P<CRC>[0-9a-fx]+)',
line)
if m:
rdict['cluster'] = m.groupdict()
continue
# Host wells (10.175.50.74) UP, service startups enabled,
m = re.search(
'Host\s+(?P<hostname>[\S]+)\s+\((?P<ip>[0-9\.]+)\)\s+(?P<state>[^,]+),\s+service\s+startups\s+(?P<service_startups>[^,]+),',
line)
if m:
currenthost = m.group('hostname')
rdict['hosts'][currenthost] = m.groupdict()
continue
# RSF-1 release 3.8.9, built on 12-Nov-2013-11:39 "3.8.9".
m = re.search(
'\s+RSF-1\s+release\s+(?P<release>[0-9\.]+),\s+built\s+on\s+(?P<built_on>[\S]+)\s+\"(?P=release)\"\.',
line)
if m:
for item in m.groupdict():
rdict['hosts'][currenthost][item] = m.groupdict()[item]
continue
# 2 nodes configured, 2 online.
m = re.search('(?P<configured>[0-9]+)\s+nodes\s+configured,\s+(?P<online>[0-9]+)\s+online.',
line)
if m:
rdict['nodes'] = m.groupdict()
continue
# 0 Service bpool3, IP address bbox-vip-1, "Coraid bpool3 ZFS service":
m = re.search(
'(?P<index>[0-9]+)\s+Service\s+(?P<name>[^,]+),\s+IP\s+address\s+(?P<ip>[^,]+),\s+\"(?P<description>[^\"]+)\":',
line)
if m:
currentservice = m.groupdict()['index']
rdict['service'][currentservice] = m.groupdict()
rdict['service'][currentservice]['hosts'] = dict()
continue
# running automatic unblocked on wells
m = re.search(
'\s+(?P<status>[\S]+)\s+(?P<mode>[\S]+)\s+(?P<state>[\S]+)\s+on\s+(?P<name>[^$]+)', line)
if m:
rdict['service'][currentservice]['hosts'][m.group('name')] = m.groupdict()
continue
# 1 service configured
m = re.search('(?P<configured>[0-9]+)\s+services\s+configured', line)
if m:
rdict['services'] = m.groupdict()
continue
# 1 service instance stopped
m = re.search('\s+(?P<stopped>[0-9]+)\s+service\s+instance\s+stopped', line)
if m:
rdict['services']['stopped'] = m.groupdict()['stopped']
continue
# 1 service instance running
m = re.search('\s+(?P<running>[0-9]+)\s+service\s+instance\s+running', line)
if m:
rdict['services']['running'] = m.groupdict()['running']
continue
# Heartbeats:
m = re.search('Heartbeats:', line)
if m:
rdict['hearbeat'] = dict()
continue
# 0 net wells -> street [10.175.50.76]: Up, last heartbeat #154376 Thu Jan 23 11:01:33
m = re.search(
'(?P<index>[0-9]+)\s+(?P<type>[\S]+)\s+(?P<source>[\S]+)\s+\->\s+(?P<destination>[^:]+):\s+(?P<state>[^,]+),\s+last\s+heartbeat\s+#(?P<last_index>[0-9]+)\s+(?P<last_timestamp>[^$]+)',
line)
if m:
rdict['hearbeat'][m.groupdict()['index']] = m.groupdict()
continue
# 1 disc wells -> street (via /dev/rdsk/c0t5000C500572F501Fd0s0:512,/dev/rdsk/c0t5000C500572F501Fd0s0:518) [(20]: Up, last heartbeat #154376 Thu Jan 23 11:01:33
m = re.search(
'(?P<index>[0-9]+)\s+(?P<type>[\S]+)\s+(?P<source>[\S]+)\s+\->\s+(?P<destination>[\S]+)\s+\(via\s+(?:/dev/rdsk/(?P<disc>[^:]+):(?P<writesector>[0-9]+),/dev/rdsk/(?P=disc):(?P<readsector>[0-9]+))\)\s+\[\([0-9]+\]:\s+(?P<state>[^,]+),\s+last\s+heartbeat\s+#(?P<last_index>[0-9]+)\s+(?P<last_timestamp>[^$]+)',
line)
if m:
rdict['hearbeat'][m.groupdict()['index']] = m.groupdict()
continue
# 6 heartbeats configured, 6 up, 0 down
m = re.search(
'(?P<configured>[0-9]+)\s+heartbeats\s+configured,\s+(?P<up>[0-9]+)\s+up,\s+(?P<down>[0-9]+)\s+down',
line)
if m:
rdict['heartbeats'] = m.groupdict()
continue
# No errors detected
m = re.search('(?P<count>[\S]+)\s+errors\s+detected', line)
if m:
if m.groupdict()['count'] == 'No':
rdict['errors'] = 0
else:
rdict['errors'] = int(m.groupdict()['count'])
return rdict
@property
def ipadm(self):
"""
Returns a dictionary with the formatted output of 'ipadm'
*** NOTE *** 'afi' is intended to represent 'address families::
{ 'lo0': { 'addr': '',
'afi': { 'v4' : { 'addr': '127.0.0.1/8',
'class-type': 'static',
'name': 'lo0/v4',
'state': 'ok',
'under': ''},
'v6' : { 'addr': '::1/128',
'class-type': 'static',
'name': 'lo0/v6',
'state': 'ok',
'under': ''}},
'class-type': 'loopback',
'name': 'lo0',
'state': 'ok',
'under': ''}
}
"""
rdict = dict()
columns = ['name', 'class-type', 'state', 'under', 'addr']
result = self.run_and_check('ipadm')
for line in re.split('\n', result.message):
if line.startswith('NAME'):
continue
l = line.split()
# change '--' to empty string
for ndx in range(len(l)):
if l[ndx] == '--':
l[ndx] = ''
if len(l) == len(columns):
if l[0].find('/') != -1:
parts = l[0].split('/')
rdict[parts[0]]['afi'][parts[1]] = dict(zip(columns, l))
else:
rdict[l[0]] = dict(zip(columns, l))
rdict[l[0]]['afi'] = dict()
return rdict
def run_and_check(self, cmd, expectation=True):
if True:
raise NotImplemented
return ReturnCode(False)
[docs]class SolarisSsh(Client, ZFSSystem, Initiator):
"""
A paramiko based solaris client.
"""
def __init__(self, *args, **kwargs):
if isinstance(args[0], dict): # this allows instantiation with a config dict item
for k, v in args[0].items():
setattr(self, k, v)
else:
self.user = args[0]
self.hostname = args[1]
self.password = args[2]
self.ethdrv = Ethdrv(self.get_ethdrv)
super(SolarisSsh, self).__init__(self.hostname, self.user, self.password)
self.os = 'solaris'
self.nsdir = '/dev/ethdrv'
def reboot(self, wait=True):
"""
reboot the intiator using 'init 6' do not return until ssh stops working
"""
cmd = "init 6"
ret = self.run_and_check(cmd)
if wait:
ret = self.wait_shutdown()
self.close()
return ret
@wait_until(sleeptime=5, timeout=60 * 60)
def wait_shutdown(self):
return self.is_down()
def is_down(self):
try:
self.run_and_check('echo waiting for ssh to shutdown', timeout=5)
except (OSError, TIMEOUT):
logger.info('ssh appears to have shutdown')
return ReturnCode(True, "host appears down")
return ReturnCode(False)
def get_ethdrv(self, fname):
"""
Required function for Ethdrv class
"""
sftpsession = self.open_sftp()
try:
fh = sftpsession.open('/dev/ethdrv/%s' % fname, 'r')
ret = ReturnCode(True)
ret.message = fh.read()
except Exception as e:
ret = ReturnCode(False, str(e))
return ret
# return self.run_and_check('cat /dev/ethdrv/%s' % fname)
def reconnect(self, after=10, timeout=10, args=None):
"""
attempts to reconnect to the host in a loop. ?BUG: This will never give up.
:param after:
:type after: float
:param timeout: length of time to wait each time
:type timeout: float
:param args: left in for compatibility with pexpect ssh
:type args: None
:return: True
"""
while 1:
with ignored(InitiatorError):
time.sleep(after)
if super(SolarisSsh, self).connect(timeout=timeout):
return True
def run_and_check(self, cmd, expectation=True, force=False, timeout=None):
"""
Run a command check the result. If the caller cares about failure, indicated by
not setting expectation to False, and the command fails we raise an exception.
"""
logger.info("calling %s" % cmd)
if force:
raise NotImplementedError
else:
result = self.run(cmd, timeout=timeout)
if not result and expectation:
logger.critical(result.message)
raise InitiatorError(result.message)
else:
return result
def put(self, localpath, remotepath=None):
"""
put a file on the initiator from the controlling host
:param localpath:
:param remotepath: If None use pwd and basename
:return:
"""
if not remotepath:
remotepath = os.path.basename(localpath)
sftpsession = self.open_sftp()
sftpsession.put(localpath, remotepath)
return
def get(self, remotepath, localpath=None):
"""
get a file from the initiator and write it to the controlling host
:param remotepath: path to remote file
:param localpath: If None use pwd and basename
:return:
"""
if not localpath:
localpath = "%s/%s" % (os.getcwd(), os.path.basename(remotepath))
sftpsession = self.open_sftp()
sftpsession.get(remotepath, localpath)
return
def verify_hba_ports_speed(self):
"""
This function verifies HBA ports speed information
:return: True if current link rate is in the output
"""
ports_result = self.run_and_check('ethdrvadm list-ports')
match = re.search('EHBA-\d+-.*?/(\d+)', str(ports_result))
if match:
speed = match.group(1)
logger.info(
"ethdrvadm list-ports command ran sucessfully showing link Type and link Speed : %s" % speed)
return True
else:
logger.info("ethdrvadm list-ports command failed, not showing link Speed... ")
return False
@property
def hba_ports(self):
"""
Returns a dictionary of the HBA's ports file contents::
{'0': {'link': {'max': '1000', 'speed': '0'},
'mac': '00100401103c',
'port': '0',
'type': 'EHBA-2-E-RJ45'},
'1': {'link': {'max': '1000', 'speed': '1000'},
'mac': '00100401103d',
'port': '1',
'type': 'EHBA-2-E-RJ45'}
}
"""
ports = dict()
r = self.run_and_check('ethdrvadm list-ports')
if not r:
return ports
lines = r.message.splitlines()
for l in lines[1:]: # We skip the header
if not l:
continue
flds = l.split()
p = flds[0]
ports[p] = dict()
ports[p]['port'] = p
ports[p]['type'] = flds[1]
ports[p]['mac'] = flds[2]
ports[p]['link'] = dict()
speed, maxi = flds[3].split('/')
ports[p]['link']['speed'] = speed
ports[p]['link']['max'] = maxi
return ports
def lun_exists(self, lun, flush=True):
"""
Returns lun's aoestat dict::
{'device': 'sd379', 'port': ['1'], 'target': '91.1', 'size': '2000.398GB'}
or False in ReturnCode format
"""
if flush:
self.aoeflush()
n = self.aoestat
if lun in n:
return ReturnCode(True, n[lun])
return ReturnCode(False, '%s not found' % lun)
@property
def uname(self):
return self.run_and_check('uname').message.rstrip()
def df(self, mount_point):
"""
Returns the amount of disk space available a given file system containing each
file name argument. Disk space is returned in human readable format (--human-readable).
"""
if not os.path.exists(mount_point):
raise Exception("Mount point %s does not exist" % mount_point)
cmd = "df -h %s" % mount_point
ret = self.run_and_check(cmd)
output = str(ret)
logger.debug(output)
header = re.compile(r"Filesystem[ \t]*Size[ \t]*Used[ \t]*Available[ \t]*Capacity[ \t]*Mounted on")
lines = output.splitlines()
info = dict()
if not header.search(lines[0]):
print("output not recognized")
else:
if len(lines) > 3:
line = " ".join(lines[1:len(lines)])
else:
line = lines[1]
[filesystem, size, used, avail, percent_used, mounted_on] = line.split()
percent_used = percent_used.replace('%', '')
info['filesystem'] = filesystem
info['size'] = size
info['used'] = used
info['avail'] = avail
info['percent_used'] = int(percent_used)
info['mounted_on'] = mounted_on
return info
@property
def rdsk(self):
"""
:return: list of all /dev/rdisks entries
:rtype:
"""
return self.run_and_check('ls /dev/rdsk').message.splitlines()
def targ2sd(self, targ):
"""
:return: the symbolic sd device name for the AoE Target
"""
ret = ''
if not targ:
return targ
n = self.lun_exists(str(targ))
if n:
ret = n.message.device
return ret
def sd2dev(self, sdname, path='/dev/rdsk/', postfix='p0'):
"""
:return: disk device or character device for a given sd name as /dev/dsk or /dev/rdsk
For zpool use set path='/dev/dsk' and postfix=''
:param sdname: the sd device name e.g. sd35
:param path: '/dev/rdsk/' or '/dev/dsk/' to toggle character device node
:param postfix: which slice or partition. Defaults to BIOS whole disk.
:return:
"""
ret = ''
if not sdname:
return sdname
cmd = "iostat -nl 1 " + sdname + "|head -n1"
result = self.run_and_check(cmd)
if result:
s = result.message.split()
if len(s) == 3:
ret = "%s%s%s" % (path, s[1], postfix)
return ret
def sd2targ(self, sd):
"""
return the aoe target address backing the given /dev/sd* device.
:param sd:
:type sd:
:return:
:rtype:
"""
devname = self.sd2dev(sd, path='', postfix='')
if not devname:
return devname
else:
devtargre = re.compile(r"c\d+t(?P<major>\d+)d(?P<minor>\d+)")
r = re.match(devtargre, devname)
if r:
r = r.groupdict()
target = AoEAddress(r['major'], r['minor'])
else:
target = None
return target
def targ2dev(self, targ, path='/dev/rdsk/', postfix='p0'):
"""
Return the device path for an aoe Target. Defaults to BIOS whole disk for use with fio's raw.
For zpool use set path='/dev/dsk' and postfix=''
:param targ: the aoe device e.g. '2.1' or as AoEAddress type
:param path: '/dev/rdsk/' or '/dev/dsk/' to toggle character device node
:param postfix: which slice or partition.
:return:
"""
n = self.targ2sd(str(targ))
if n:
return self.sd2dev(n, path, postfix)
else:
return ''
def _target2device(self, t):
"""
try to identify a device name for a particular target.
"""
device = self.run_and_check('ls /dev/rdsk/c*t%sd%s*' % tuple(t.split('.')))
rregex = re.match('/dev/rdsk/c(\d+)t%sd%s' % tuple(t.split('.')), device.message)
if rregex:
return 'c%s' % rregex.group(1) + 't%sd%s' % tuple(t.split('.'))
else:
return None