polardbxengine/storage/ndb/mcc/clusterhost.py

867 lines
34 KiB
Python

#Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
#
#This program is free software; you can redistribute it and/or modify it under the terms of the GNU
#General Public License as published by the Free Software Foundation; version 2 of the License.
#
#This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
#even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
#General Public License for more details.
#
#You should have received a copy of the GNU General Public License along with this program; if not,
#write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
"""Tools for performing tasks on (possibly remote) Cluster hosts."""
import time
import posixpath
import ntpath
import abc
import os
import socket
import subprocess
import tempfile
import shutil
import logging
import platform
import json
import shlex
import util
from util import bcolors
import request_handler
_logger = logging.getLogger(__name__)
# Lock for executing commands fast on localhost.
# CMD_LOCK = threading.Lock()
class ExecException(Exception):
"""Exception type thrown when process-spawning fails on the local host. """
def __init__(self, cmd, exitstatus, out):
self.cmd = cmd
self.exitstatus = exitstatus
self.out = out.read()
def __str__(self):
return bcolors.FAIL + 'Command ' + self.cmd + ' exited with ' + self.exitstatus + ':\n' + \
self.out + bcolors.ENDC.format
class HostInfo(object):
"""Class which provides host information from a Linux-style /proc file system."""
def __init__(self, ch, uname, machine, osver, osflavor):
self.ch = ch
self.pm = posixpath
self.uname = uname
self.machine = machine
self.envcmd = ['env']
self.osver = osver
self.osflavor = osflavor
@property
def _host_info_path(self):
return self.path_module.join(request_handler.CONFIGDIR, 'host_info', 'binaries', self.uname,
self.machine, 'host_info')
def _run_host_info(self):
host_res = json.loads(self.ch._exec_pkg_cmdv([self._host_info_path]))
host_res['uname'] = self.uname
return {'host': {'name' : self.ch.host}, 'hostRes': host_res}
@property
def ram(self):
"""Caching?"""
with self.ch.open('/proc/meminfo') as meminfo:
return int(meminfo.readline().split()[1]) // 1024
@property
def cores(self):
"""x"""
with self.ch.open('/proc/cpuinfo') as cpuinfo:
return len([ln for ln in cpuinfo.readlines() if 'processor' in ln])
@property
def installdir(self):
"""x"""
return None # Don't know how to get this for remote hosts
@property
def homedir(self):
"""x"""
return self.ch.env['HOME']
@property
def path_module(self):
"""Returns the python path module to use when manipulating path names on this host."""
return self.pm
@property
def disk_free(self):
"""Returns the free space for homedir on host."""
hd = self.ch.env['HOME']
dtest = self.ch.exec_blocking(['df', '-h', hd])
#There is a chance there will be an error output from opening the console.
# grab df output, skip to last line, go one back and grab Avail (4th).
lc = dtest.count('\n')
#There has to be 2 lines of df output + errors from console (if any).
if lc > 1:
ds = str(((dtest).split('\n')[lc-1]).split()[3:4]).strip('[]').strip("''")
if len(ds) >= 2:
return ds
wrn_msg = 'CHS--> Length of parsed df output was less than 2 characters wide.'
_logger.warning(bcolors.WARNING + wrn_msg + bcolors.ENDC)
return 'unknown'
else:
wrn_msg = 'CHS--> df output had less than 2 lines.'
_logger.warning(bcolors.WARNING + wrn_msg + bcolors.ENDC)
return 'unknown'
@property
def docker_info(self):
"""Returns NOT INSTALLED, NOT RUNNING or RUNNING for Docker."""
# We will ask for this info if necessary. IF docker is requested and it's NOT INSTALLED, we
# will install and run docker. IF docker NOT RUNNING we will start it. IF docker is RUNNING,
# front-end will do nothing. All *nix platforms share same code.
d_test = None
try:
d_test = self.ch.exec_blocking(['docker', '-v'])
_logger.debug('CHS--> docker -v finished with dtest = %s', d_test)
except:
_logger.debug('CHS--> docker -v failed with dtest = %s', d_test)
d_test = ""
#We ignore ver. info for now and send new request for result of command.
if d_test != "":
#Now check if docker is running.
try:
d_test = self.ch.exec_blocking(['ps', '-e', '-o', 'comm', '|', 'grep', '[d]ocker'])
_logger.debug('CHS--> ps finished with dtest = %s', d_test)
except:
wrn_msg = 'CHS--> ps failed with dtest = %s'
_logger.warning(bcolors.WARNING + wrn_msg + bcolors.ENDC, d_test)
d_test = ""
if d_test != "":
return 'RUNNING'
return 'NOT RUNNING'
return 'NOT INSTALLED'
@property
def intip(self):
"""Returns the internal IP address of the host. Should work for all but Mac and Win."""
_logger.debug('CHS--> Running ifconfig')
d_test = None
if isinstance(self.ch, LocalClusterHost):
# localhost providing addressed via external IP can still be part of Cluster
# requiring InternalIP
d_test = subprocess.Popen([
r'ifconfig -a | grep -A 1 "RUNNING" | grep -v "LOOPBACK" | grep -v "127\.0\.0\.1"' +
r' | grep -oP "(?<=inet\s)\d+(\.\d+){3}"'], stdout=subprocess.PIPE, shell=True)
dt = d_test.communicate()[0]
return str(dt).split("\n")
else:
d_test = self.ch.exec_blocking(
['/usr/sbin/ifconfig', '-a', '|', 'grep', '-A', '1', '"RUNNING"', '|', 'grep', \
'-v', '"LOOPBACK"', '|', 'grep', '-v', r'"127\.0\.0\.1"', \
'|', 'grep', '-oP', r'"(?<=inet\s)\d+(\.\d+){3}"'])
if d_test is None:
_logger.warning(bcolors.WARNING + 'CHS--> ifconfig failed.' + bcolors.ENDC)
return ''
lc = d_test.count('\n')
# Oracle cloud machines return single internal IP address! There is 1+ ip
# addresses, return that and let FE sort things out.
if lc >= 1:
return d_test
wrn_msg = 'CHS--> ifconfig output had less than 2 lines.'
_logger.warning(bcolors.WARNING + wrn_msg + bcolors.ENDC)
return ''
class SolarisHostInfo(HostInfo):
"""Specialization for Solaris which uses prtconf and psrinfo to get host information."""
@property
def ram(self):
return int(self.ch.exec_blocking(['/usr/sbin/prtconf']).split()[7])
@property
def cores(self):
return len(
self.ch.exec_blocking(['/usr/sbin/prtconf']).split('\n')[0:-1])
@property
def intip(self):
return "unknown"
class MacHostInfo(HostInfo):
"""Specialization for MacOS which uses sysctl to get host information."""
@property
def ram(self):
sysinfo = self.ch.exec_blocking(['/usr/sbin/sysctl', 'hw.'])
return [int(filter(str.isdigit, ln.split()[1])) for ln \
in sysinfo.split('\n') if 'hw.memsize:' in ln][0] // 1024 // 1024
@property
def cores(self):
sysinfo = self.ch.exec_blocking(['/usr/sbin/sysctl', 'hw.'])
return [int(filter(str.isdigit, ln.split()[1])) for ln in \
sysinfo.split('\n') if 'hw.ncpu:' in ln][0]
@property
def intip(self):
d_test = None
dt = []
if isinstance(self.ch, LocalClusterHost):
_logger.debug('CHS--> ifconfig local Mac')
d_test = subprocess.Popen(
["ifconfig -a | grep -A 4 'RUNNING' | grep 'inet' | grep -v" +
r" 'LOOPBACK' | grep -v '127\.0\.0\.1' | grep -v 'inet6' | " +
"awk '''{ print $2 }'''"], stdout=subprocess.PIPE, shell=True)
dt = d_test.communicate()[0]
return str(dt).split("\n")
else:
d_test = self.ch.exec_blocking(
['ifconfig', '-a', '|', 'grep', '-A', '4', '"RUNNING"', '|', 'grep', '"inet"',\
'|', 'grep', '-v', '"LOOPBACK"', '|', 'grep', '-v', r'"127\.0\.0\.1"', '|',\
'grep', '-v', '"inet6"'])
if d_test is None:
_logger.warning(bcolors.WARNING + 'CHS--> ifconfig failed.' + bcolors.ENDC)
return ''
lc = d_test.count('\n')
dt1 = []
if lc >= 1:
dt1 = d_test.split('\n')
for x in xrange(len(dt1)):
if 'inet ' in dt1[x]:
_logger.debug('CHS--> Appending intIP:%s',
dt1[x].lstrip().split('inet ')[1].split(' ')[0])
dt.append(dt1[x].lstrip().split('inet ')[1].split(' ')[0])
return dt
wrn_msg = 'CHS--> ifconfig output had less than 2 lines.'
_logger.warning(bcolors.WARNING + wrn_msg + bcolors.ENDC)
return ''
class CygwinHostInfo(HostInfo):
"""Specialization for Cygwin which uses systeminfo and wmic to retrieve host information, but
retains posixpath as the path module."""
@property
def _host_info_path(self):
return self.path_module.join('install', 'host_info', 'Windows', 'host_info.exe')
@property
def ram(self):
cmdv = ['cmd.exe', '/c', 'wmic', 'ComputerSystem', 'get', 'TotalPhysicalMemory', '/value']
rd = self.ch.exec_blocking(cmdv).split("\r") # TotalPhysicalMemory=34190766080
ram = ""
for t in rd:
if t.strip() != "":
if t.find("TotalPhysicalMemory") != -1:
ram = t.split("TotalPhysicalMemory=")[1]
break
return int(ram) // 1024 // 1024 # Return MB same as systeminfo would.
@property
def cores(self):
"""Returns number of logical CPU's available on box."""
wmic = self.ch.exec_blocking(['C:/Windows/System32/Wbem/wmic', 'CPU', 'GET', '/VALUE'])
return sum(
[int(ln.split('=')[1]) for ln in wmic.split('\n') if 'NumberOfLogicalProcessors' in ln])
@property
def docker_info(self):
"""Returns state (NOT INSTALLED, NOT RUNNING and RUNNING) of docker."""
cmdv = ['C:/Windows/System32/sc.exe', 'queryex', 'com.docker.service']
# This one is expected to fail mostly with 1060:
# The specified service does not exist as an installed service.
d_test = self.ch.exec_cmdv(cmdv, {'noRaise': 1060, 'waitForCompletion': True}, None)
if len(d_test) > 10:
if d_test.find("STATE") != -1:
gds = d_test.split("STATE :")[1].split("WIN32_EXIT_CODE")[0].strip()
if gds.find("4 RUNNING") != -1:
return "RUNNING"
return "NOT RUNNING"
else:
return "NOT INSTALLED"
else:
# Actually this is error in sc but the result is the same.
return 'NOT INSTALLED(error)'
@property
def intip(self):
mt = self.ch.exec_blocking(['C:/Windows/System32/ipconfig', '/all'])
if len(mt) > 6:
tmp = [ln for ln in mt.split("\r\n") if "IPv4" in ln]
return [ln.split(":")[1].replace("(Preferred)", "").strip() for ln in tmp]
return "unknown"
class WindowsHostInfo(CygwinHostInfo):
"""Specialization of CygwinHostInfo for native Windows which uses ntpath as the path module."""
def __init__(self, ch, uname, machine, osver, osflavor):
super(type(self), self).__init__(ch, uname, machine, osver, osflavor)
self.pm = ntpath
self.envcmd = ['cmd.exe', '/c', 'set']
@property
def homedir(self):
en = self.ch.env
if en.has_key('USERPROFILE'):
return en['USERPROFILE']
return en['HOMEDRIVE']+en['HOMEPATH']
@property
def path_module(self):
return ntpath
@property
def disk_free(self):
en = self.ch.env
if en.has_key('USERPROFILE'):
hd = en['USERPROFILE']
else:
hd = en['HOMEDRIVE']+en['HOMEPATH']
mt = (self.ch.exec_blocking(['C:/Windows/System32/Wbem/wmic', 'logicaldisk', hd[:2],
'GET', 'freespace'])).split('\n')[1]
s = ""
for i in range(len(mt)-1):
if ord(mt[i]) >= 48:
s += str(mt[i])
if len(s) < 2:
return 'unknown'
try:
return str(int(s) // 1024 // 1024 // 1024) + 'G'
except (TypeError, ValueError): # hope that's all
return 'unknown'
@property
def intip(self):
""" Returns (Array of) internal IP address(es)."""
# This should be it:
mt = self.ch.exec_blocking(['C:/Windows/System32/ipconfig', '/all'])
if len(mt) > 6:
tmp = [ln for ln in mt.split("\r\n") if "IPv4" in ln]
return [
ln.split(":")[1].replace("(Preferred)", "").strip() for ln in tmp]
return "unknown"
# Map from uname string to HostInfo type
HOST_INFO_MAP = {'SunOS' : SolarisHostInfo,
'Darwin' : MacHostInfo,
'Windows' : WindowsHostInfo,
'CYGWIN' : CygwinHostInfo}
class ABClusterHost(object):
"""Base class providing common interface."""
__meta_class__ = abc.ABCMeta
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.drop()
return False
def __init__(self):
self._hostInfo = None
self._env = None
@abc.abstractmethod
def _get_system_tuple(self):
"""Return a string identifying the hosts operating system."""
pass
@abc.abstractmethod
def _exec_pkg_cmdv(self, cmdv):
"""Execute a package binary on this ClusterHost."""
pass
@abc.abstractmethod
def open(self, filename, mode='r'):
"""Abstract"""
pass
@abc.abstractmethod
def get(self, host, filename, localname):
"""Abstract"""
pass
@abc.abstractmethod
def put(self, host, localname, filename):
"""Abstract"""
pass
@property
def hostInfo(self):
"""Create an appropriate HostInfo object for localhost. Uses Pythons platform.system() to
determine the type of HostInfo object to return."""
if self._hostInfo is None:
(system, machine, osver, osflavor) = self._get_system_tuple()
if HOST_INFO_MAP.has_key(system):
self._hostInfo = HOST_INFO_MAP[system](self, system, machine, osver, osflavor)
else:
dbg_msg = 'CHS--> Using default HostInfo for host %s (%s, %s)'
_logger.debug(dbg_msg, self.host, system, machine)
return HostInfo(self, system, machine, osver, osflavor)
return self._hostInfo
@property
def path_module(self):
"""Path module to use when manipulating file paths."""
return self.hostInfo.path_module
@property
def env(self):
"""environment"""
if not self._env:
ctx = {'str': self.exec_blocking(self.hostInfo.envcmd), 'properties': {}}
util.parse_properties(ctx)
self._env = ctx['properties']
return self._env
@property
def ram(self):
"""Returns total amount of RAM in MB"""
return self.hostInfo.ram
@property
def cores(self):
"""Returns how many logical processors host has."""
return self.hostInfo.cores
@property
def uname(self):
"""UNAME for host."""
return self.hostInfo.uname
@property
def installdir(self):
"""Where to find MySQL executables"""
return self.hostInfo.installdir
@property
def homedir(self):
"""HOME for logged in user"""
return self.hostInfo.homedir
@property
def disk_free(self):
"""Free disk space on HOME partition or 'unknown'."""
return self.hostInfo.disk_free
@property
def docker_info(self):
"""Return Docker service status on host; 'RUNNING', 'NOT RUNNING' or 'NOT INSTALLED'"""
return self.hostInfo.docker_info
@property
def intip(self):
"""Depending on how many IP addresses various NET commands filtered out, we return single
or array of IP addresses deemed as "Internal"""
return self.hostInfo.intip
@abc.abstractmethod
def drop(self, paths=[]):
"""Close open connections and remove files.
paths - list of files to remove from host before closing connection
"""
map(self.rm_r, paths)
@abc.abstractmethod
def file_exists(self, path):
"""Test for the existence of a file. If the file actually exists, its stat object is
returned, otherwise None.
path - file to check the existence of
"""
pass
@abc.abstractmethod
def list_dir(self, path):
"""List the files in a directory.
path - directory to list
"""
pass
@abc.abstractmethod
def mkdir_p(self, path):
"""Provides mkdir -p type functionality. I.e, missing parent directories are also created.
If the directory we are trying to create already exists, we silently do nothing. If path or
any of its parents is not a directory an exception is raised.
path - directory to create on remote host """
pass
@abc.abstractmethod
def rm_r(self, path):
"""Provides rm -r type functionality. All files and directories are removed recursively.
path - file or directory to remove."""
pass
def auto_complete(self, basedir, locations, executable):
"""Find the absolute path of an executable given a prefix directory, a set of possible
directories, and the basename of the executable.
basedir - basedir of a cluster installation
locations - list of directories to try
executable - basename of executable to auto-complete"""
for l in locations:
choice = posixpath.join(basedir, l, executable)
_logger.debug('Testing if %s exists.', choice)
if self.file_exists(choice):
_logger.debug('%s exists.', choice)
return choice
else:
_logger.debug('%s doesn\'t exist.', choice)
raise Exception('Cannot locate '+executable+' in '+
posixpath.join(basedir, str(locations)) + ' on host ' + self.host)
@abc.abstractmethod
def _exec_cmdv(self, cmdv, procCtrl, std_in_file):
pass
def _execfast(self, cmdv):
pass
def execfast(self, cmdv):
"""Forwards to virtual."""
return self._execfast(cmdv)
def exec_cmdv(self, cmdv, procCtrl={'waitForCompletion': True}, std_in_file=None):
"""Forwards to virtual."""
return self._exec_cmdv(cmdv, procCtrl, std_in_file)
def exec_blocking(self, cmdv):
"""Convenience method."""
assert(isinstance(cmdv, list))
return self.exec_cmdv(cmdv, {'waitForCompletion': True})
def exec_cluster_daemon(self, cluster_daemon, waitsec):
"""Convenience method."""
assert(isinstance(cluster_daemon, list))
return self.exec_cmdv(cluster_daemon, {'daemonWait': waitsec})
class LocalClusterHost(ABClusterHost):
"""Implement the ABClusterHost interface for access to the localhost without using SSH over
Paramiko. Note that this implies that there will be no authentication."""
def __init__(self, host):
super(type(self), self).__init__()
self.host = host
self.osver = None
self.osflavor = None
self.hlpstr = None
self.system = None
def _get_system_tuple(self):
if self.system is not None:
return (self.system, self.hlpstr, self.osver, self.osflavor)
osver = None
osflavor = None
hlp_str = None
system = platform.system()
if system == 'Windows':
try:
subprocess.check_call(['uname'])
except WindowsError:
_logger.debug('CHS--> No uname available, assuming native Windows')
# systeminfo slows things down significantly.
m = platform.platform()
#system = m.split("-")[0]
osver = m.split("-")[1]
osflavor = "Microsoft " + system + " " + osver
self.osver = osver
self.osflavor = osflavor
self.hlpstr = platform.uname()[-2]
self.system = system
return (system, platform.uname()[-2], osver, osflavor)
else:
self.osver = osver
self.osflavor = osflavor
self.hlpstr = 'Unknown'
self.system = 'CYGWIN'
return ('CYGWIN', 'Unknown', osver, osflavor)
#System is either Linux, SunOS or Darwin
if system.startswith('Darwin'):
osver = str(self.exec_blocking(['uname', '-r']))
osflavor = "MacOSX"
if system.startswith("SunOS"): #or system.startswith("Solaris"):
osver = str(self.exec_blocking(['uname', '-v']))
osflavor = "Solaris"
if "Linux" in system:
system = "Linux"
# Assumption is all Linux flavors will have /etc/os-release file.
if self.file_exists('/etc/os-release'):
hlp_str = self.exec_blocking(['test', '-f', '/etc/os-release'])
else:
hlp_str = "0"
if hlp_str != "0":
hlp_str = self.exec_blocking(['cat', '/etc/os-release'])
matched_lines = [line for line in hlp_str.split('\n') if "ID=" in line]
hlp = (str(matched_lines[0]).split("ID=", 1)[1]).strip('"')
osflavor = hlp
matched_lines = [line for line in hlp_str.split('\n') if "VERSION_ID=" in line]
hlp = (str(matched_lines[0]).split("VERSION_ID=", 1)[1]).strip('"')
osver = hlp
else:
#Bail out, no file
wrn_msg = 'CHS--> %s, %s, %s does not have /etc/os-release file!'
_logger.warning(bcolors.WARNING + wrn_msg + bcolors.ENDC, system, osflavor, osver)
self.osver = osver
self.osflavor = osflavor
self.hlpstr = platform.uname()[-1]
self.system = system
return (system, platform.uname()[-1], osver, osflavor)
def _exec_pkg_cmdv(self, cmdv):
"""Locally this just forwards to exec_cmdv."""
return self.exec_cmdv(cmdv)
@property
def env(self):
return os.environ
@property
def cores(self):
return self.hostInfo.cores
@property
def installdir(self):
system = platform.system()
if system == 'Windows':
# On Windows, there is no path auto-complete and the startup script (ndb_setup.py)
# returns BASEDIR so we need to add \bin.
pat = request_handler.BASEDIR
if not "\bin" in pat or not "/bin" in pat:
pat = os.path.join(pat, 'bin')
return pat
else:
return request_handler.BASEDIR
def open(self, filename, mode='r'):
"""Open a file on ABClusterHost."""
return open(filename, mode)
def get(self, filename, localname):
"""Get a file on ABClusterHost and save to ~/.mcc."""
#File names are complete with path.
return shutil.copy(filename, localname)
def put(self, localname, filename):
"""Copy file."""
#File names are complete with path.
return shutil.copy(localname, filename)
def drop(self, paths=[]):
"""Close open connections and remove files.
paths - list of files to remove from host before closing connection
"""
map(self.rm_r, paths)
def file_exists(self, path):
"""Test for the existence of a file on the local host. If the file
actually exists, its stat result object is returned, otherwise None.
path - file to check the existence of
"""
if os.path.exists(path):
return os.stat(path)
return None
def list_dir(self, path):
"""List the files in a directory on the local host. Forwards to os.listdir().
path - directory to list
"""
return os.listdir(path)
def mkdir_p(self, path):
"""Provides mkdir -p type functionality on localhost. Does nothing if
the directory already exists, otherwise forwards to os.makedirs.
path - directory to create on remote host
"""
if os.path.exists(path) and os.path.isdir(path):
return
os.makedirs(path)
def rm_r(self, path):
"""Provides rm -r type functionality on localhost. Forwards to os.rmdirs.
path - file or directory to remove
"""
shutil.rmtree(path)
def _execfast(self, cmdv):
"""Quick and dirty exec for localhost. Not sure locking is even needed."""
# global CMD_LOCK
# with CMD_LOCK:
proc = subprocess.Popen(cmdv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
shell=True)
out = proc.communicate()[0]
rc = proc.returncode
if rc == 0:
return out
return 'errcode:'+str(rc)+'\n' + out
def _exec_cmdv(self, cmdv, procCtrl, stdin_file):
"""Execute an OS command on the local host, using subprocess module.
cmdv - complete command vector (argv) of the OS command
procCtrl - procCtrl object from message which controls how the process
is started (blocking vs non-blocking and output reporting)
"""
# Add nohup when running locally to prevent server from waiting on the children
if util.get_val(procCtrl, 'nohup'):
cmdv[:0] = ['nohup']
stdin = None
if stdin_file != None:
stdin = self.open(stdin_file)
#We need Popen to have all possible FE commands to work. Using single mechanism to run all
# commands adds to readability. I am hoping to reduce number of exec_cmd functions too :-/
# Several possible cases:
# 1) !waitForCompletion and !daemonWait: Just fire and forget.
# 2) waitForCompletion and !daemonWait: Wait for result.
# 3) waitForCompletion and daemonWait: Wait for result but poll after daemonWait seconds.
# 4) !waitForCompletion and daemonWait: Wait for result only daemonWait seconds.
# Old code was partial 2) and 3)+4).
#New code:
# Wait for daemonWait AND MAX 120 seconds for command to return but check if it finished
# every second (after waiting daemonWait).
#Rationale: We have commands that can not return immediately even on localhost. For such
# commands we set daemonWait member of procControl to appropriate amount of seconds (say 2)
# forcing this exec routine to always wait that long for response. After that, we allow
# maximum of 120s for command to complete before we kill it. If command does not have
# procControl.daemonWait this means we expect entire process to finish as soon as command
# returns. For such commands, we wait max 120s.
proc_DW_tot = 120
proc_DW = 0
if procCtrl.has_key('daemonWait'):
proc_DW = util.get_val(procCtrl, 'daemonWait')
tst = ""
if isinstance(cmdv, list):
tst = ' '.join(cmdv)
else:
tst = str(cmdv)
_logger.debug("CHS--> daemonWait is %s", proc_DW)
_logger.debug("CHS--> cmdv %s", tst)
# hasKill = procCtrl.has_key('kill'). Commands with Kill member have different processing.
kill_it = True
try:
if isinstance(cmdv, list):
proc = subprocess.Popen(cmdv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
else:
proc = subprocess.Popen(shlex.split(cmdv), stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
# There are commands we have to wait for to start with, such as NET START/STOP etc.
_logger.debug("CHS--> proc %s executed", tst)
if proc_DW > 0:
_logger.debug("CHS--> proc %s executed, sleeping", tst)
time.sleep(proc_DW)
#We have waited set amount of time, now see if it returns in 2min and if not, kill it.
for t in xrange(proc_DW_tot):
time.sleep(0.5)
if proc.poll() is not None:
kill_it = False
_logger.debug("CHS--> %ss, breaking for %s", t/2, tst)
if proc_DW > 0:
dbg_msg = "CHS--> Waited %ssec (on top of %ssec) for %s to complete."
_logger.debug(dbg_msg, t/2, proc_DW, tst)
else:
_logger.debug("CHS--> Waited %s seconds for %s to complete.", t/2, tst)
break
if kill_it:
_logger.warning(bcolors.WARNING + "CHS--> killing %s" + bcolors.ENDC, tst)
proc.kill()
retcode = proc.returncode
if retcode is None:
retcode = 1
rd = proc.stdout.read()
wrn_msg = "CHS--> killing, raising %s rc %s, msg %s"
_logger.warning(bcolors.WARNING + wrn_msg + bcolors.ENDC, tst, retcode, rd)
raise ExecException(tst, retcode, proc.stdout)
else:
retcode = proc.returncode
rd = proc.stdout.read()
if retcode is None: #REALLY BAD except if it's meant to be so, such as with KILL -9
retcode = retcode or -1
rd = rd or "No output from command."
wrn_msg = "CHS--> killing, raising %s rc %s, msg %s"
_logger.warning(bcolors.WARNING + wrn_msg + bcolors.ENDC, tst, retcode, rd)
raise ExecException(tst, proc.returncode, proc.stdout)
elif retcode == 0 or (retcode != 0 and \
retcode == util.get_val(procCtrl, 'noRaise')):
_logger.debug("CHS--> Conditional succ. noRise:%s.", retcode or -1)
# When successful, cut it to command and 100 characters of response. To remove
# just empty lines and not lines with spaces:
#"".join([s for s in t.strip().splitlines(True) if s.strip("\r\n")])
rd = rd or "No output from command." # rd can be None, for example for KILL -9
_logger.debug("CHS--> Succ. %s(%s), %s...", tst, retcode, \
"".join([t for t in rd.strip().splitlines(True) if t.strip()])[0:100])
return rd
elif retcode <= 125:
wrn_msg = "CHS--> %s failed, exit-code=%d error = %s"
_logger.warning(bcolors.WARNING + wrn_msg + bcolors.ENDC, tst, retcode, rd)
return 'errcode:'+str(retcode)+'\n' + rd
elif retcode == 127:
wrn_msg = bcolors.WARNING + "CHS--> %s, program not found: %s" + bcolors.ENDC
_logger.warning(wrn_msg, tst, rd)
return 'errcode:'+str(retcode)+'\n' + rd
else:
# Things get hairy and unportable - different shells return
# different values for coredumps, signals, etc.
rd = rd or "No output from command." # rd can be None
_logger.warning(bcolors.WARNING + "CHS--> %s, program reported OS-dependant "
"exit-code=%d error = %s" + bcolors.ENDC, tst, retcode, rd)
return 'errcode:'+str(retcode)+'\n' + rd
finally:
if stdin is not None:
stdin.close()
def execute_command(self, cmdv, inFile=None):
"""Execute an OS command blocking on the local host, using subprocess module. Returns dict
containing output from process.
cmdv - complete command vector (argv) of the OS command."""
out_file = tempfile.TemporaryFile()
err_file = tempfile.TemporaryFile()
# This will fail if command needs shell-expansion. Also, no timeout.
result = {
'exitstatus': subprocess.call(args=cmdv, stdin=inFile, stdout=out_file, stderr=err_file)
}
out_file.seek(0)
err_file.seek(0)
result['out'] = out_file.read()
result['err'] = err_file.read()
_logger.debug('CHS--> execute_command %s on localhost, result %s', ' '.join(cmdv), result)
return result
def produce_ABClusterHost(hostname='localhost', key_based=None, user=None, pwd=None, key_file=None):
"""Factory method which returns RemoteClusterHost or LocalClusterHost depending on the value of
hostname.."""
_logger.debug('CHS--> Produce ABC, hostname passed %s', hostname)
shn = socket.gethostbyname_ex(socket.gethostname())
#Say ('host.dom.comp.com', [], ['10.17.16.24'])
_logger.debug('CHS--> Produce ABC socket host name is %s.', shn)
if hostname == 'localhost' or hostname == '127.0.0.1' or hostname == shn[0]:
_logger.debug('CHS--> Host is local.')
return LocalClusterHost(hostname)
for x in range(len(shn[2])):
if hostname == shn[2][x]:
return LocalClusterHost(hostname)
# Sanitize input:
if not (user and user.strip()):
user = None
if not (pwd and pwd.strip()):
pwd = None
else:
if pwd.startswith('**'):
pwd = None
if not (key_file and key_file.strip()):
key_file = None
import remote_clusterhost
_logger.debug('CHS--> Producing remote ABC.')
return remote_clusterhost.RemoteClusterHost(hostname, key_based, user, pwd, key_file)