#!/usr/bin/env python2.7
"""
Linux
-----
A paramiko based Linux initiator module.
"""
import os
import re
import time
import logging
from collections import defaultdict
import gzip
import cStringIO
from otto.connections.ssh import Client
from otto.initiators.ethdrv import Ethdrv
from otto.lib.decorators import wait_until
from otto.lib.otypes import InitiatorError, ReturnCode, Namespace, AoEAddress
instance = os.environ.get('instance') or ''
logger = logging.getLogger('otto' + instance + '.initiators')
logger.addHandler(logging.NullHandler())
def _expand_path(path):
return '"$(echo %s)"' % path
# _escape_for_regex is from Fabric there are other ideas from Fabric in here.
# Copyright (c) 2009-2015 Jeffrey E. Forcier
# Copyright (c) 2008-2009 Christian Vest Hansen
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
def _escape_for_regex(text):
"""Escape ``text`` to allow literal matching using egrep"""
regex = re.escape(text)
# Seems like double escaping is needed for \
regex = regex.replace('\\\\', '\\\\\\')
# Triple-escaping seems to be required for $ signs
regex = regex.replace(r'\$', r'\\\$')
# Whereas single quotes should not be escaped
regex = regex.replace(r"\'", "'")
return regex
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 discover > /proc/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('ethdrv-flush %s' % ('', '-a')[aflag])
@property
def aoestat(self):
"""
Returns a dictionary of either the 'aoe-stat' output, or
the 'ethdrv-stat' output (based on self.coraid_module) in the
following format::
{'1270.0': {'claim': None,
'device': None,
'file': 'sde',
'ifs': [0, 1],
'iounit': None,
'path': '/dev/sde',
'paths': None,
'port': [0, 1],
'size': '100.030GB',
'state': None,
'target': '1270.0',
'targpath': {0: {'address': ['00259096645f', '00259096645e'],
'port': 0
},
1: {'address': ['00259096645f', '00259096645e'],
'port': 1
}
}
}
}
"""
stat = 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})})
cmd = '%s-stat -a | gzip' % self.coraid_module
r = self.run_and_check(cmd)
r.message = gzip.GzipFile('', 'r', 0, cStringIO.StringIO(r.message)).read()
if self.coraid_module == "ethdrv":
if self._aoeversion:
v = self._aoeversion
else:
v = self.aoeversion
lines = r.message.splitlines()
for l in lines:
f = l.split()
if f[0].startswith('e'):
# the target column changed between 5.2.2 and 5.2.3
if v['major'] == 5:
if v['minor'] > 2 or (v['minor'] == 2 and v['revision'] >= 3):
target = f[0][1:]
device = f[1]
else:
target = f[1][1:]
device = f[0]
elif v['major'] > 5:
target = f[0][1:]
device = f[1]
else:
target = f[1][1:]
device = f[0]
stat[target]['target'] = target
stat[target]['file'] = device
stat[target]['path'] = "/dev/%s" % device # deprecated
stat[target]['size'] = f[2]
if f[3].find('N/A') is -1:
f[3] = [int(n) for n in f[3].split(',')]
else:
f[3] = list()
stat[target]['port'] = f[3]
stat[target]['ifs'] = stat[target]['port'] # deprecated
else:
p = int(f[0])
f[1] = f[1].strip(',')
stat[target]['targpath'][p].update({'port': p, 'address': list(f[1:])})
continue
elif self.coraid_module == "aoe":
lines = str(r).split('\n')
for l in lines:
f = l.split()
if not l or not len(f) > 3:
continue
targ = f[0]
if targ.startswith('e'):
targ = targ[1:]
stat[targ] = {}
stat[targ]['target'] = targ
stat[targ]['file'] = f[0]
stat[targ]['path'] = "/dev/etherd/%s" % f[0] # deprecated
stat[targ]['size'] = f[1]
stat[targ]['port'] = f[2].split(',')
for i in range(len(stat[targ]['port'])):
if stat[targ]['port'][i].startswith('eth'):
stat[targ]['port'][i] = stat[targ]['port'][i][3:]
stat[targ]['ifs'] = stat[targ]['port'] # deprecated
if len(f) > 4:
stat[targ]['iounit'] = f[3]
stat[targ]['state'] = f[4]
else:
# the iounit column is missing as of aoe6-79pre4
stat[targ]['iounit'] = ""
stat[targ]['state'] = f[3]
# logger.debug("aoestat: %s" % str(stat))
return Namespace(stat)
@property
def aoeversion(self):
"""
Returns the version string output
of the AoE driver specified by self.coraid_module.
"""
ret = {}
if self.coraid_module == 'ethdrv':
cmd = 'ethdrv-release'
elif self.coraid_module == 'aoe':
cmd = 'aoe-version'
else:
raise InitiatorError("unknown module type %s" % self.coraid_module)
vers = str(self.run_and_check(cmd))
if self.coraid_module == "ethdrv":
# root@python2 ~# ethdrv-release
# 5.2.6-R0
# root@python2 ~#
mmrr = r"^([0-9]+)\.([0-9]+)\.([0-9]+)\-R([0-9]+)?"
m = re.match(mmrr, vers)
if m:
ret = {'major': int(m.group(1)),
'minor': int(m.group(2)),
'revision': int(m.group(3)),
'release': 0}
if m.lastindex == 4:
ret['release'] = int(m.group(4))
else:
# root@python2 ~# ethdrv-release
# installed ethdrv driver: 6.0.1-R5
# running ethdrv driver: 6.0.1-R5
# root@python2 ~#
for l in vers.split('\n'):
if not l:
continue
m = re.match(r"[ \t]*(installed|running) ethdrv driver:[ \t]+(.*)", l)
if m:
ret[m.group(1)] = m.group(2)
# maj, min, rev, & rel default to running version,
# but if not loaded, fall back to installed version
m2 = re.match(mmrr, m.group(2))
if m2:
ret['major'] = int(m2.group(1))
ret['minor'] = int(m2.group(2))
ret['revision'] = int(m2.group(3))
ret['release'] = 0
if m2.lastindex == 4:
ret['release'] = int(m2.group(4))
else:
for l in vers.split('\n'):
if not l:
continue
ls = l.split(':')
key = ls[0].strip()
# keep "aoetools" as a key, but isolate
# "installed" from "installed aoe driver"
# and "running" from "running aoe driver"
flds = key.split()
if len(flds) > 1:
key = flds[0].strip()
ret[key] = ls[1].strip()
self._aoeversion = ret
return ret
def run_and_check(self, cmd, expectation=True):
if True:
raise NotImplementedError("implemented by the child class")
return ReturnCode(False)
def _stat_device_match(shelf_lun, stat):
"""
If shelf_lun is set to "all", then return all device
names. If shelf_lun does not contain a '.', then return
a list of all device names on the specified shelf. If
shelf_lun contains a '.', then return the device name
only for the specified target, as a string.
"""
devs = []
shelf_lun = str(shelf_lun)
hasdot = shelf_lun.find('.') != -1
if hasdot:
devs = ""
for targ in stat:
if hasdot:
# specific target
if stat[targ]['target'] == shelf_lun:
devs = stat[targ]['path']
break
elif shelf_lun == 'all':
# all targets on all shelves
devs.append(stat[targ]['path'])
else:
# all targets on this shelf only
shelf = str(AoEAddress(stat[targ]['target']).shelf)
if shelf_lun == shelf:
devs.append(stat[targ]['path'])
logger.debug("_stat_device_match: %s" % str(devs))
return devs
[docs]class LinuxSsh(Client, Initiator):
"""
LinuxSsh
--------
A class for controlling a linux initiator using SSH/SFTP services directly.
This object can be instantiated with a dictionary::
lnx = {'user':'root', 'host: 'localhost','password':'passw0rd!'}
cfg = Namespace({'lnx_host1': lnx})
# the above simulates the config object
my_linux = LinuxSsh(cfg.lnx_host1)
my_linux.connect()
"""
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():
if k == 'mount':
setattr(self, 'mount_point', v)
setattr(self, k, v)
else:
self.user = args[0]
self.hostname = args[1]
self.password = args[2]
self.mount_point = kwargs.get('mount')
self.ethdrv = Ethdrv(self.get_ethdrv)
super(LinuxSsh, self).__init__(self.hostname, self.user, self.password)
self.os = 'linux'
self.nsdir = '/proc/ethdrv'
if not hasattr(self, 'coraid_module'):
self.coraid_module = "ethdrv"
Initiator.__init__(self, self.coraid_module)
[docs] def get_ethdrv(self, fname):
"""
Required function for Ethdrv class
"""
return self.run_and_check('cat /proc/ethdrv/%s' % fname)
[docs] def run_and_check(self, cmd, expectation=True, force=False, timeout=None, bufsize=-1):
"""
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("%s called", cmd)
if force:
raise NotImplementedError
else:
result = self.run(cmd, timeout=timeout, bufsize=bufsize)
if result is not None:
result.message = result.message.rstrip()
if not result and expectation:
logger.critical(result.message)
raise InitiatorError(result.message)
else:
return result
[docs] def put(self, localpath, remotepath=None):
"""
:param localpath:
:param remotepath: If None use pwd and basename
:return:
"""
if not remotepath:
remotepath = os.path.basename(localpath)
sftpsession = self.open_sftp()
return sftpsession.put(localpath, remotepath)
[docs] def get(self, remotepath, localpath=None):
"""
:param remotepath:
: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()
return sftpsession.get(remotepath, localpath)
@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('ethdrv-ports')
if not r:
return ports
lines = r.message.splitlines()
for l in lines: # We skip the header at the top and the prompt at the bottom.
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
[docs] def list_dir(self, absolute_path):
"""
If a directory, returns a ReturnCode with status=True and a list of directory contents
If not, status=False, message=list()
"""
dirlist = list()
try:
out = self.run_and_check("ls %s" % absolute_path)
except InitiatorError:
return ReturnCode(False, list())
for file in out.message.splitlines():
dirlist.append(file)
return ReturnCode(True, dirlist)
[docs] def lun_exists(self, lun, flush=True):
"""
If found returns a ReturnCode with status=True and lun's aoestat dict::
{'device': 'sd379', 'port': ['1'], 'target': '91.1', 'size': '2000.398GB'}
when not, False and "<lun> not found" in message.
"""
if flush:
self.aoeflush()
n = self.aoestat
if lun in n:
return ReturnCode(True, n[lun])
return ReturnCode(False, '%s not found' % lun)
[docs] 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]*Avail[ \t]*Use%[ \t]*Mounted on")
lines = output.splitlines()
info = dict()
if not header.search(lines[0]):
logger.critical("df: 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 a list of all the raw block devices. (Solaris compatibility).
:rtype: list of str
"""
return self.run_and_check(r"ls /dev/sd* | grep -e '\/dev\/sd.*[^0-9]$'").message.splitlines()
@property
def uname(self):
return self.run_and_check('uname').message.strip()
@property
def distro(self):
"""
check what distribrution we are dealing with
"""
rel = self.run_and_check("cat /etc/redhat-release")
if "red hat" in rel.message.lower():
return "redhat"
elif "centos" in rel.message.lower():
return "centos"
[docs] def targ2sd(self, targ):
"""
Return the symbolic sd device name for the AoE Target.
"""
if not targ:
return targ
ret = self.aoestat.get(targ)
if ret:
ret = ret['file']
return ret
@staticmethod
[docs] def sd2dev(sdname, path='/dev/'):
"""
Return disk device or character device for a given sd name.
This is included for script cross compatibility with Solaris
:param sdname: the sd device name e.g. sd35
:return: dev path
"""
return "%s%s" % (sdname, path)
[docs] def sd2targ(self, sd):
"""
Return the aoe target associated with an sd device. If none found None is returned.
"""
ret = self.aoestat
for target, v in ret.iteritems():
if v.get('file') == sd:
return AoEAddress(target)
return None
[docs] def targ2dev(self, targ):
"""
Return the device path for an aoe Target.
:param targ: the aoe device e.g. '2.1' or as AoEAddress type
:return:
"""
if self.coraid_module == "aoe":
return '/dev/etherd/e' + str(targ)
else:
return '/dev/ethdrv/e' + str(targ)
@property
def packages(self):
ret = self.run_and_check('rpm -qa --qf "%{NAME}\n"')
return ret.message.splitlines()
[docs] def exists(self, path):
"""
Return True if given path exists on the current remote host.
"""
cmd = 'test -e %s' % _expand_path(path)
return self.run_and_check(cmd, expectation=False)
[docs] def contains(self, filename, text, exact=False, escape=True):
"""
Return True if ``filename`` contains ``text`` (which may be a regex.)
By default, this function will consider a partial line match (i.e. where
``text`` only makes up part of the line it's on). Specify ``exact=True`` to
change this behavior so that only a line containing exactly ``text``
results in a True return value.
This function leverages ``egrep`` on the remote end (so it may not follow
Python regular expression syntax perfectly), and skips ``env.shell``
wrapper by default.
If ``use_sudo`` is True, will use `sudo` instead of `run`.
If ``escape`` is False, no extra regular expression related escaping is
performed (this includes overriding ``exact`` so that no ``^``/``$`` is
added.)
"""
if escape:
text = _escape_for_regex(text)
if exact:
text = "^%s$" % text
egrep_cmd = 'egrep "%s" %s' % (text, _expand_path(filename))
return self.run_and_check(egrep_cmd, expectation=False)
[docs] def append(self, filename, text, partial=False, escape=True):
"""
Append string (or list of strings) ``text`` to ``filename``.
When a list is given, each string inside is handled independently (but in
the order given.)
If ``text`` is already found in ``filename``, the append is not run, and
None is returned immediately. Otherwise, the given text is appended to the
end of the given ``filename`` via e.g. ``echo '$text' >> $filename``.
The test for whether ``text`` already exists defaults to a full line match,
e.g. ``^<text>$``. You may override this and force partial searching
(e.g. ``^<text>``) by specifying ``partial=True``.
Because ``text`` is single-quoted, single quotes will be transparently
backslash-escaped. This can be disabled with ``escape=False``.
"""
# Normalize non-list input to be a list
if isinstance(text, basestring):
text = [text]
for line in text:
regex = '^' + _escape_for_regex(line) + ('' if partial else '$')
if self.exists(filename) and line and self.contains(filename, regex, escape=False):
continue
line = line.replace("'", r"'\\''") if escape else line
return self.run_and_check("echo '%s' >> %s" % (line, _expand_path(filename)))
[docs] def appendUUID(self, UUID, mnt_path, fstype):
"""
Append with UUID a device entry at the end of /etc/fstab file
:param UUID: the UUID of a device
:type UUID: str
:param mnt_path: where it should be mounted
:type mnt_path: str
:param fstype: what kind of filesystem is on thta device
:type fstype: str
:return: success status
:rtype: bool
"""
return self.append("/etc/fstab", "%s %s %s defaults 1 2\n" % (UUID, mnt_path, fstype))
[docs] def e2fsck(self, device_path):
"""
Run e2fsck -f on a device.
"""
return self.run_and_check("e2fsck -f %s" % device_path)
[docs] def findmount(self, blocks):
"""
Findmount looks through mtab and returns
a list of all matching mounts.
"""
cmd = 'cat /etc/mtab'
ret = self.run_and_check(cmd)
mtab = str(ret)
mtabl = mtab.split('\n')
mnts = []
if type(blocks) == str:
blocks = [blocks]
for b in blocks:
for m in mtabl:
f = m.split()
if len(f) > 1 and f[0] == b:
mnts.append(f[1])
return mnts
[docs] def fsck(self, device_path, repair=False):
"""
Check and optionally repair a file system.
"""
if repair:
cmd = "fsck %s" % device_path
else:
cmd = "fsck -n %s" % device_path
return self.run_and_check(cmd)
[docs] def get_device_size(self, device):
"""
Returns ReturnCode object
call linux's 'blockdev' cmd-line tool
to get the device size in bytes.
"""
cmd = "blockdev --getsize64 %s" % device
ret = self.run_and_check(cmd, expectation=False)
if ret:
output = str(ret)
logger.debug(output)
output = int(output)
return output
else:
return None
@property
def ipaddr(self):
"""
:return: the ip address of the interface we are connected to
:rtype: str
"""
return self.get_transport().sock.getpeername()[0]
[docs] def get_ipv4_addr(self, eth=0):
"""
The Linux way to get an ether's IP address.
Given this, return the IPv4 address '10.220.70.4':
[root@pickles smokemonster]# ifconfig
eth0 Link encap:Ethernet HWaddr 00:25:90:0A:3C:04
inet addr:10.220.70.4 Bcast:10.220.255.255 Mask:255.255.0.0
"""
r = self.run("ifconfig eth%s" % str(eth))
if not r:
return r
triplet = "[0-9]{1,3}"
regex = "inet addr:(%s.%s.%s.%s)" % (triplet, triplet, triplet, triplet)
m = re.search(regex, r.message)
if m:
ipv4 = m.group(1)
return ReturnCode(True, ipv4)
else:
return ReturnCode(False, "no IPv4 address found: %s" % r.message)
[docs] def get_mac_addr(self, eth=0):
"""
The Linux way to get a ether mac addr.
"""
r = self.run("ifconfig eth%s" % str(eth))
if not r:
return r
h2 = "[0-9a-fA-F]{2}"
regex = "HWaddr[ \t]+(%s):(%s):(%s):(%s):(%s):(%s)" % (h2, h2, h2, h2, h2, h2)
m = re.search(regex, r.message)
if m:
mac = "%s%s%s%s%s%s" % (m.group(1), m.group(2), m.group(3),
m.group(4), m.group(5), m.group(6))
return ReturnCode(True, mac)
else:
return ReturnCode(False, "no mac addr found: %s" % r.message)
[docs] def get_pids(self, name):
"""
Getpids returns a list of process IDs matching name.
"""
r = self.run_and_check("ps -C %s -o pid=" % name)
if not r:
return []
return r.message.split()
[docs] def get_UUID(self, dev_name):
"""
return UUID for device name
:param dev_name: device file name under /dev
:type dev_name: str
:return: the UUID for device
:rtype: str
"""
cmd = 'blkid /dev/%s' % dev_name
logger.debug("Running command %s", cmd)
blkid_result = self.run_and_check(cmd, expectation=False)
if not blkid_result:
return None
if blkid_result.find("UUID=") > -1:
logger.debug(blkid_result)
blkentry = r'UUID="(?P<uuid>[a-f,0-9,\-]*?)"\s+TYPE="(?P<type>.*?)"'
m = re.search(blkentry, blkid_result.message)
if m:
UUID = m.group('uuid')
logger.debug("UUID for device name %s is %s", dev_name, UUID)
return UUID
else:
logger.debug("UUID not found for device name %s ", dev_name)
return False
[docs] def install_rpm(self, package_path, timeout=30, expectation=True):
"""
This function will install the rpm
:rtype: boolean
"""
cmd = "rpm -i %s" % package_path
ret = self.run_and_check(cmd, timeout=timeout, expectation=expectation)
if ret.find("Failed dependencies") > -1:
logger.debug(ret)
return False
else:
return ret
[docs] def is_package_installed(self, package_name):
"""
This function will find if the package is installed on the host machine
and return the list of packages installed
Argument : package_name (eg : 'ethdrv')
Return : True,installed package list, or
False, []
"""
return package_name in self.packages
[docs] def lsmod(self):
"""
return loaded modules
:rtype: list
"""
cmd = 'lsmod'
modules = list()
ret = self.run_and_check(cmd).message.splitlines()
for module in ret:
modules.append(module.split()[0])
return modules
@property
def memory(self):
"""
Returns the total memory of the machine as an integer in KB.
"""
sz = 0
cmd = 'cat /proc/meminfo'
ret = self.run_and_check(cmd)
o = ret.message
m = re.search(r"MemTotal: +([0-9]+) ", o)
if m:
sz = int(m.group(1))
else:
logger.error("memory: %s", o)
return sz
[docs] def mkfs(self, device, fstype="ext3", force=False, expectation=True):
"""
Create a file system on device. By default ext3 is used.
"""
fflag = ""
if force:
if fstype == "xfs":
fflag = "-f "
elif fstype == "ext3" or fstype == "ext4":
fflag = "-F "
cmd = "mkfs -t %s %s%s 2>&1" % (fstype, fflag, device)
ret = self.run_and_check(cmd, expectation=expectation, timeout=None)
return ret
[docs] def mount(self, source, fstype="ext3", target=None, max_attempts=1, expectation=True):
"""
Mounts a file system of a given type on a directory.
"""
if self.mount_point:
base = self.mount_point
else:
base = "/mnt"
if target is None:
temp_share_name = re.sub(r"\W", "_", source)
target = os.path.join(base, temp_share_name)
ret = self.mkdir(target)
if not ret:
raise InitiatorError(ret.message)
# if not os.path.exists(target):
# os.makedirs(target)
mounted = False
nchecks = 0
cmd = "mount -t %s %s %s" % (fstype, source, target)
src_split = source.split('/')
lvsource = '/dev/mapper/%s-%s' % (src_split[2], src_split[3])
logger.info(cmd)
while not mounted:
nchecks += 1
r = self.run_and_check('mount')
current_mounts = str(r)
self.run_and_check('mount')
pattern = "^%s on %s" % (source, target)
lvpattern = "^%s on %s" % (lvsource, target)
if re.search(pattern, current_mounts, re.MULTILINE) or re.search(lvpattern, current_mounts, re.MULTILINE):
mounted = True
else:
r = self.run_and_check(cmd)
if nchecks == max_attempts:
if not r:
if expectation:
raise Exception(r.message)
else:
return None
if nchecks > 1:
time.sleep(2)
return target
[docs] def load_module(self, module):
"""
This function will load the module using 'modprobe' linux command
:rtype: ReturnCode
"""
cmd = 'modprobe %s' % module
return self.run_and_check(cmd)
@staticmethod
[docs] def removeUUID(mnt_path):
"""
This function remove UUID from /etc/fstab file
:type: ReturnCode
TODO: use sed method
"""
# self.get('/etc/fstab', '.')
# logger.debug("/etc/fstab File is copied to current directory")
# fh_r = open('fstab', 'r')
# lines = fh_r.readlines()
# output = []
# for line in lines:
# if not mnt_path in line:
# output.append(line)
# fh_r.close()
#
# fh_w = open('fstab', 'w')
# fh_w.writelines(output)
# fh_w.close()
# self.put('fstab', '/etc')
# logger.debug("UUID entry for mount path %s is removed from /etc/fstab file", mnt_path)
# os.system('rm fstab')
return False
[docs] def sync(self):
"""
Force changed blocks to disk, and update the super block.
"""
return self.run_and_check('sync')
[docs] def umount(self, pathname):
"""
Unmount a file system
:param pathname: directory to unmount
:type pathname: str
:rtype: ReturnCode
"""
logger.debug("umount %s", pathname)
mounted = True
number_of_checks = 1
ret = ReturnCode('Broken while loop in unmount')
while mounted:
number_of_checks += 1
ret = self.run_and_check('mount')
output = ret.message
pattern = " on %s " % pathname
if re.search(pattern, output, re.MULTILINE):
ret = self.run_and_check('umount %s' % pathname)
time.sleep(2)
else:
mounted = False
if number_of_checks > 1:
ret = ReturnCode("After %s checks %s not found" % (number_of_checks, pathname))
time.sleep(5)
if self.exists(pathname):
ret = self.rmdir(pathname)
return ret
[docs] def uninstall_package(self, package, timeout=30):
"""
This function will uninstall the package and unload the module
Return: True for successful
False for Failure
"""
cmd = 'rpm -ev %s' % package
return self.run_and_check(cmd, timeout=timeout)
[docs] def unload_module(self, module):
"""
This function will unload the module using 'rmmod' linux command
Return : True or False
"""
return self.run_and_check("rmmod %s" % module)
[docs] def untar_package(self, src_directory, dest_directory='.'):
"""
Untar the src_directory\*.tar.gz in dest_directory
Return : True or False
"""
cmd = "tar -xvzf %s -C %s" % (src_directory, dest_directory)
return self.run_and_check(cmd)
@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, EOFError, InitiatorError):
logger.info('ssh appears to have shutdown')
return ReturnCode(True, "host appears down")
return ReturnCode(False)
def reboot(self, wait=True):
ret = self.run_and_check('reboot', False)
if wait:
ret = self.wait_shutdown()
self.close()
return ret
[docs] def sed(self, filename, before, after, limit='', backup='.bak', flags=''):
"""
Run a search-and-replace on ``filename`` with given regex patterns.
Equivalent to ``sed -i<backup> -r -e "/<limit>/ s/<before>/<after>/<flags>g"
<filename>``. Setting ``backup`` to an empty string will, disable backup
file creation.
For convenience, ``before`` and ``after`` will automatically escape forward
slashes, single quotes and parentheses for you, so you don't need to
specify e.g. ``http:\/\/foo\.com``, instead just using ``http://foo\.com``
is fine.
If ``use_sudo`` is True, will use `sudo` instead of `run`.
The ``shell`` argument will be eventually passed to `run`/`sudo`. It
defaults to False in order to avoid problems with many nested levels of
quotes and backslashes. However, setting it to True may help when using
``~fabric.operations.cd`` to wrap explicit or implicit ``sudo`` calls.
(``cd`` by it's nature is a shell built-in, not a standalone command, so it
should be called within a shell.)
Other options may be specified with sed-compatible regex flags -- for
example, to make the search and replace case insensitive, specify
``flags="i"``. The ``g`` flag is always specified regardless, so you do not
need to remember to include it when overriding this parameter.
"""
# Characters to be escaped in both
for char in "/'":
before = before.replace(char, r'\%s' % char)
after = after.replace(char, r'\%s' % char)
# Characters to be escaped in replacement only (they're useful in regexen
# in the 'before' part)
for char in "()":
after = after.replace(char, r'\%s' % char)
if limit:
limit = r'/%s/ ' % limit
context = {'script': r"'%ss/%s/%s/%sg'" % (limit, before, after, flags),
'filename': _expand_path(filename),
'backup': backup,
'extended_regex': '-E'}
expr = r"sed -i%(backup)s %(extended_regex)s -e %(script)s %(filename)s"
command = expr % context
return self.run_and_check(command)
def _uniqscsi(self, target):
"""
Ensure that the ethdrv-stat dev files for target are unique. If there are many targets,
populating dev tree with targets may bbe slow so many entries many initially appear under
/dev/sda, or after version 5.2.2, many can be listed as 'init'.
"""
stat = self.aoestat
if target == 'all':
i = 0
self.aoeflush()
while 1:
wait = 0
devs = []
if 'init' in stat:
wait = 1
else:
for targ in stat:
devs.append(stat[targ]['file'])
if len(devs) != len(set(devs)):
wait = 1
if wait == 0:
return stat
i += 1
if i % 10 == 0:
logger.debug("ethdrv-flush after %d waits", i)
self.aoeflush()
time.sleep(1)
stat = self.aoestat
else:
i = 0
wait = 1
while 1:
targdev = ''
devs = []
for targ in stat:
if targ == target:
wait = 0
targdev = stat[targ]['file']
if stat[targ]['file'] == 'init':
wait = 1
else:
devs.append(stat[targ]['file'])
if targdev in devs:
wait = 1
if wait == 0:
break
i += 1
if i % 10 == 0:
logger.debug("ethdrv-flush after %d waits", i)
self.aoeflush()
time.sleep(1)
stat = self.aoestat
return stat
[docs] def target_is_available(self, target):
"""
Check if a target is visible
"""
if self.coraid_module == "aoe":
self.aoediscover()
stat = self.aoestat
else:
stat = self._uniqscsi(target)
ret = ReturnCode(False)
for targ in stat:
if stat[targ]['target'] == target:
ret.message = stat[targ]
ret.status = True
return ret
@wait_until()
[docs] def wait_target_is_available(self, target):
"""
waits for the target be visible
"""
ret = self.target_is_available(target)
return ret
[docs] def wipe_partition_table(self, path=None, lun=None, count=1):
"""
If you are using a whole disk device for your physical volume, the disk must
have no partition table.
For DOS disk partitions, the partition id should be set to 0x8e using the fdisk
or cfdisk command or an equivalent.
For whole disk devices only the partition table must be erased, which will
effectively destroy all data on that disk.
You can remove an existing partition table by zeroing the first sector with
the following command::
# dd if=/dev/zero of=PhysicalVolume bs=512 count=1
"""
if not isinstance(path, str) and not isinstance(lun, str):
return ReturnCode(False, "Incompatible parameters: %s %s" % (path, lun))
if path is None:
path = self.aoestat.get(lun)['path']
if path is None:
return ReturnCode(False, "Couldn't find path to target %s" % lun)
cmd = 'dd if=/dev/zero of=%s bs=512 count=%s' % (path, count)
return self.run_and_check(cmd)
def __info(self, infotype=None, name=None):
if infotype not in ('pv', 'vg', 'lv'):
logger.error("No or incorrect info type specified %s", infotype)
return ReturnCode(False, "No or incorrect info type specified %s" % infotype)
if name:
cmd = '%ss --nameprefixes %s' % (infotype, name)
else:
cmd = '%ss --nameprefixes' % infotype
vols = self.run_and_check(cmd)
info = dict()
ss = vols.message.splitlines()
errors = list()
unknowns = 0
for s in ss:
if 'error' in s or 'uuid' in s:
errors.append(s)
continue
elif '_' in s:
vals = list()
col = s.strip()
col = col.replace("'0 '", "'0'")
col = col.split()
if 'unknown device' in s:
key = "'unknown%s'" % unknowns
unknowns += 1
col.pop(0)
col.pop(0)
else:
key = col.pop(0)
for pair in col:
k, v = pair.split('=')
vals.append(v[1:v.rindex("'")])
key = key[(key.index("'") + 1):key.rindex("'")]
info[key] = dict(zip(header, vals))
else:
header = s
header = header.strip().split()
header.pop(0)
if errors:
info['errors'] = errors
return info
def pvinfo(self, pvname=None):
return self.__info(infotype='pv', name=pvname)
def pvcreate(self, path=None, lun=None):
# One of these variables must be defined
if not isinstance(path, str) and not isinstance(lun, str):
return ReturnCode(False, "Incompatible parameters: %s %s" % (path, lun))
if path is None:
path = self.aoestat.get(lun)['path']
if path is None:
return ReturnCode(False, "Couldn't find path to target %s" % lun)
cmd = 'pvcreate -f %s' % path
return self.run_and_check(cmd)
def pvremove(self, path=None, lun=None):
if not isinstance(path, str) and not isinstance(lun, str):
return ReturnCode(False, "Incompatible parameters: %s %s" % (path, lun))
if path is None:
path = self.aoestat.get(lun)['path']
if path is None:
return ReturnCode(False, "Couldn't find path to target %s" % lun)
cmd = "pvremove -f %s" % path
return self.run_and_check(cmd)
def vginfo(self, vgname=None):
return self.__info(infotype='vg', name=vgname)
def vgcreate(self, vgname, devs=None, path=None):
if not isinstance(devs, list) and not isinstance(path, str):
return ReturnCode(False, "Incompatible parameters: %s %s" % (devs, path))
cmd = 'vgcreate -f %s ' % vgname
if type(path) is list:
path = ' '.join(path)
if not path:
path = str()
stat = self.aoestat
# list of devices
for d in devs:
path += " " + stat.get(d)['path']
if path is None:
logger.error("No path for lun %s" % d)
continue
cmd += '%s ' % path
return self.run_and_check(cmd)
def vgmerge(self, vg_orig, vg_add):
cmd = 'vgmerge %s %s' % (vg_orig, vg_add)
return self.run_and_check(cmd)
def vgremove(self, vgname):
cmd = 'vgremove -f %s ' % vgname
return self.run_and_check(cmd)
def vgreduce(self, vgname, removemissing=False):
cmd = 'vgreduce '
if removemissing:
cmd += '--removemissing '
cmd += vgname
return self.run_and_check(cmd)
def lvinfo(self, vgname=None):
return self.__info(infotype='lv', name=vgname)
def lvconvert(self, vgname, repair=False):
cmd = 'lvconvert '
if repair:
cmd += '-y --repair '
cmd += vgname
return self.run_and_check(cmd)
def lvcreate(self, vgname, lvname, lv_size='100%FREE', stripe_sz=None, mirrors=0):
cmd = 'lvcreate '
if "FREE" in lv_size:
cmd += '-l %s ' % lv_size
else:
cmd += '-L %s ' % lv_size
if stripe_sz:
vgs = self.vginfo(vgname=vgname)
npv = vgs[vgname]['#PV']
cmd += '-i %s -I %s ' % (npv, stripe_sz)
if mirrors:
cmd += '-m %s ' % mirrors
cmd += '-n %s %s' % (lvname, vgname)
return self.run_and_check(cmd)
def lvextend(self, lvpath, size=None, percentage='+100%FREE'):
if size:
cmd = 'lvextend -L %s %s' % (size, lvpath)
else:
cmd = 'lvextend -l %s %s' % (percentage, lvpath)
return self.run_and_check(cmd)
def lvremove(self, lvpath):
cmd = 'lvremove -f %s' % lvpath
return self.run_and_check(cmd)
def resize2fs(self, lvpath):
cmd = 'resize2fs %s' % lvpath
return self.run_and_check(cmd, timeout=1200)
# Ctl and its dependencies
# Todo: Breakout ctl into its own, nonblocking object
[docs] def ctl(self, rw, key, skip, shelf_lun, total=None):
"""
Ctl first verifies that the target exists, then reads
from/writes to it using 'ctl'.
"""
shelf_lun = str(shelf_lun)
self.verifytarget(shelf_lun)
b = self.get_block_devices(shelf_lun)
cmd = "ctl -%s -k %s -s %s" % (rw, key, skip)
if total:
cmd += " -T %s" % total
cmd += " %s" % b
return self.run(cmd)
def __uniqscsiall(self):
"""
Ensure that the ethdrv-stat dev files are unique. Especially
if there are many targets, loading targets may take a while,
so many entries appear under /dev/sda, or after version
5.2.2, many can be listed as 'init'.
"""
i = 0
self.aoeflush()
while 1:
wait = 0
stat = self.aoestat
devs = []
if 'init' in stat:
wait = 1
else:
for targ in stat:
devs.append(stat[targ]['file'])
if len(devs) != len(set(devs)):
wait = 1
if wait == 0:
return stat
i += 1
if i % 10 == 0:
logger.debug("ethdrv-flush after %d waits" % i)
self.aoeflush()
time.sleep(1)
def __uniqscsi(self, target):
"""
Ensure that the ethdrv-stat dev files for target are unique.
Especially if there are many targets, loading targets may
take a while, so many entries appear under /dev/sda, or after
version 5.2.2, many can be listed as 'init'.
"""
if target == 'all':
return self.__uniqscsiall()
i = 0
wait = 1
while 1:
targdev = ''
stat = self.aoestat
devs = []
for targ in stat:
if targ == target:
wait = 0
targdev = stat[targ]['file']
if stat[targ]['file'] == 'init':
wait = 1
else:
devs.append(stat[targ]['file'])
if targdev in devs:
wait = 1
if wait == 0:
return stat
i += 1
if i % 10 == 0:
logger.debug("ethdrv-flush after %d waits" % i)
self.aoeflush()
time.sleep(1)
@wait_until(timeout=300, sleeptime=5)
[docs] def verifytarget(self, target):
"""
Verifytarget waits for the target to show up
in the aoe-stat or ethdrv-stat output, whichever
is defined in self.coraid_module variable.
"""
logger.debug("verifying target: %s ..." % target)
self.aoediscover()
target = str(target)
modul = self.coraid_module
if modul == 'aoe':
stat = self.aoestat
else:
stat = self.__uniqscsi(target)
for targ in stat:
if stat[targ]['target'] == target and stat[targ]['ifs'][0] != "N/A":
return stat[targ]
[docs] def get_block_devices(self, address):
"""
return a list of all /dev/ names for a specific shelf.lun, or
a list of block files for all targets on a shelf, or
if address=="all", all targets on all shelves.
"""
# should this be an iterator that yields devices as they appear?
if self.coraid_module == "ethdrv":
ret = self._shelf2scsi(address)
else:
ret = self._shelf2etherd(address)
retls = list()
if type(ret) == str:
retls = [ret]
elif type(ret) == list:
retls = ret
# give each device 5 secs to mount after seeing it in aoestat
for r in retls:
i = 0
while not os.path.exists(r) and i < 5:
i += 1
time.sleep(1)
return ret
def _shelf2etherd(self, shelf_lun):
"""
Return where in the namespace linux has mounted AoE addressed
targets in the form of:
/dev/etherd/e01.5
"""
stat = self.aoestat
return _stat_device_match(shelf_lun, stat)
def _shelf2scsi(self, shelf_lun):
"""
Return a scsi device node for the given AoE Addressed LUN.
If 'shelf_lun' is 'all', then return a list of all device
names. If 'shelf_lun' does not contain a '.', then return
a list of all device names on the specified shelf. If
'shelf_lun' contains a '.', then return the device name
only for the specified target, as a string. Returns a path
with the form: /dev/sd[a-zA-Z]+.
"""
stat = self.__uniqscsi(shelf_lun)
return _stat_device_match(shelf_lun, stat)