316 lines
12 KiB
Python
316 lines
12 KiB
Python
# Copyright (c) 2012, 2013 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
|
|
|
|
"""Provides specialization of ABClusterHost for remote hosts using Paramiko."""
|
|
|
|
import errno
|
|
import stat
|
|
import util
|
|
import time
|
|
import paramiko
|
|
import ntpath
|
|
import logging
|
|
import os.path
|
|
import tempfile
|
|
import contextlib
|
|
import posixpath
|
|
|
|
import clusterhost
|
|
from clusterhost import ABClusterHost
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
def quote_if_contains_space(s):
|
|
if ' ' in s:
|
|
return '"'+s+'"'
|
|
return s
|
|
|
|
class RemoteExecException(clusterhost.ExecException):
|
|
"""Exception type thrown whenever os-command execution fails on
|
|
a remote host. """
|
|
|
|
def __init__(self, hostname, cmd, exitstatus, out):
|
|
self.hostname = hostname
|
|
self.cmd = cmd
|
|
self.exitstatus = exitstatus
|
|
self.out = out.read()
|
|
def __str__(self):
|
|
return 'Command `{self.cmd}\', running on {self.hostname} exited with {self.exitstatus}:\n{self.out}'.format(self=self)
|
|
|
|
class RemoteClusterHost(ABClusterHost):
|
|
"""Implements the ABClusterHost interface for remote hosts. Wraps a paramiko.SSHClient and uses
|
|
this to perform tasks on the remote host."""
|
|
|
|
def __init__(self, host, username=None, password=None):
|
|
super(type(self), self).__init__()
|
|
self.host = host
|
|
self.user = username
|
|
self.pwd = password
|
|
c = paramiko.SSHClient()
|
|
c.load_system_host_keys()
|
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
c.connect(hostname=self.host, username=self.user, password=self.pwd)
|
|
self.__client = c
|
|
self.__sftp = c.open_sftp()
|
|
|
|
def close(self):
|
|
self.drop()
|
|
|
|
@property
|
|
def client(self):
|
|
return self.__client
|
|
|
|
@property
|
|
def sftp(self):
|
|
return self.__sftp
|
|
|
|
# @property
|
|
# def client(self):
|
|
# """"A freshly connected SSHClient object."""
|
|
# if self.__client != None:
|
|
# if self.__sftp != None:
|
|
# self.__sftp.close()
|
|
# self.__sftp = None
|
|
# self.__client.close()
|
|
|
|
# c = paramiko.SSHClient()
|
|
# c.load_system_host_keys()
|
|
|
|
# # TODO - we need user acceptance for this by button in the frontend
|
|
# c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
# ak = 'H:\\.ssh\\known_hosts'
|
|
# if os.path.exists(ak):
|
|
# _logger.debug('Loading additional host keys from %s', ak)
|
|
# c.load_host_keys(filename=ak)
|
|
# else:
|
|
# _logger.debug('File %s does not exist here', ak)
|
|
|
|
# c.connect(hostname=self.host, username=self.user, password=self.pwd)
|
|
# self.__client = c
|
|
# return c
|
|
|
|
# @property
|
|
# def sftp(self):
|
|
# """"An SFTPClient object to this host. It and its SSHClient
|
|
# object will be created on demand."""
|
|
# if self.__sftp != None:
|
|
# self.__sftp.close()
|
|
|
|
# self.__sftp = self.client.open_sftp()
|
|
# return self.__sftp
|
|
|
|
def _get_system_tuple(self):
|
|
preamble = None
|
|
system = None
|
|
processor = None
|
|
try:
|
|
preamble = self.exec_blocking(['#'])
|
|
except:
|
|
_logger.debug('executing # failed - assuming Windows...')
|
|
(system, processor) = self.exec_blocking(['cmd.exe', '/c', 'echo', '%OS%', '%PROCESSOR_ARCHITECTURE%']).split(' ')
|
|
if 'Windows' in system:
|
|
system = 'Windows'
|
|
else:
|
|
_logger.debug('preamble='+preamble)
|
|
raw_uname = self.exec_blocking(['uname', '-sp'])
|
|
_logger.debug('raw_uname='+raw_uname)
|
|
uname = raw_uname.replace(preamble, '', 1)
|
|
_logger.debug('uname='+uname)
|
|
(system, processor) = uname.split(' ')
|
|
if 'CYGWIN' in system:
|
|
system = 'CYGWIN'
|
|
return (system, processor.strip())
|
|
|
|
def _exec_pkg_cmdv(self, cmdv):
|
|
"""For remote hosts the binary is fist copied over using sftp."""
|
|
_logger.debug("%s", str(self.sftp.listdir()))
|
|
hi = os.path.basename(cmdv[0])
|
|
self.sftp.put(cmdv[0], hi)
|
|
self.sftp.chmod(hi, stat.S_IRWXU)
|
|
return self.exec_cmdv([self.path_module.join('.', hi)] + cmdv[1:-1])
|
|
|
|
def _sftpify(self, path):
|
|
"""Since sftp treats all path names as relative to its root we must
|
|
convert absolute paths before using them with sftp. As quick-fix we
|
|
assume that the sftp root is equal to the drive letter of all absolute
|
|
paths used. If it isn't the sftp operations will fail."""
|
|
return self.path_module.splitdrive(path)[1]
|
|
|
|
def open(self, filename, mode='r'):
|
|
"""Forward to paramiko.SFTPClient.open for remote hosts.
|
|
Wrap in contextlib.closing so that clients can use
|
|
with-statements on it."""
|
|
return contextlib.closing(self.sftp.open(self._sftpify(filename), mode))
|
|
|
|
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)
|
|
if self.__sftp:
|
|
self.__sftp.close()
|
|
self.__sftp = None
|
|
if self.__client:
|
|
self.__client.close()
|
|
self.__client = None
|
|
|
|
def file_exists(self, path):
|
|
"""Test for the existence of a file on the remote host. If the file actually exists,
|
|
its stat object is returned, otherwise None.
|
|
path - file to check the existence of
|
|
"""
|
|
try:
|
|
return self.sftp.stat(self._sftpify(path))
|
|
except IOError as ioerr:
|
|
if ioerr.errno == errno.ENOENT:
|
|
return None
|
|
_logger.debug('stat failure on '+path)
|
|
raise
|
|
|
|
def list_dir(self, path):
|
|
"""List the files in a directory on the remote host. Forwards to
|
|
SFTPClient.listdir(), but also warns about empty results that may be caused
|
|
by paramiko not reporting missing execute permission on the directory
|
|
correctly.
|
|
path - directory to list
|
|
"""
|
|
content = self.sftp.listdir(self._sftpify(path))
|
|
if len(content) == 0:
|
|
m = stat.S_IMODE(self.sftp.stat(path).st_mode)
|
|
for role in ['USR', 'GRP', 'OTH']:
|
|
mask = util.get_fmask('R', role)|util.get_fmask('X',role)
|
|
if (m & mask) != mask:
|
|
_logger.debug('Directory '+path+' does not have both read and execute permission for ' + role + '.\nIf you depend on '+role+ ' for access, the empty directory listing may not be correct')
|
|
|
|
return content
|
|
|
|
def mkdir_p(self, path):
|
|
"""Provides mkdir -p type functionality on the remote host. That is,
|
|
all 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
|
|
"""
|
|
_logger.debug('mkdir_p('+path+')')
|
|
path = self._sftpify(path)
|
|
pa = self.file_exists(path)
|
|
if pa != None:
|
|
#print str(pa)+" "+str(pa.st_mode)
|
|
if not util.is_dir(pa):
|
|
raise Exception(self.host+':'+path+' is not a directory')
|
|
return
|
|
# Need to user normpath here since dirname of a directory with a trailing slash
|
|
# is the directory without a slash (a dirname bug?)
|
|
sd = ntpath.splitdrive(path)
|
|
_logger.debug('sd='+str(sd))
|
|
if sd[1] == '':
|
|
_logger.debug('path='+path+' is a drive letter. Returning...')
|
|
return
|
|
|
|
np = self.path_module.normpath(path)
|
|
parent = self.path_module.dirname(np)
|
|
assert parent != path
|
|
self.mkdir_p(parent)
|
|
self.sftp.mkdir(np)
|
|
|
|
def rm_r(self, path):
|
|
"""Provides rm -r type functionality on the remote host. That is, all files and
|
|
directories are removed recursively.
|
|
path - file or directory to remove
|
|
"""
|
|
path = self._sftpify(path)
|
|
if util.is_dir(self.sftp.stat(path)):
|
|
for f in self.sftp.listdir(path):
|
|
self.rm_r(self.posixpath.join(path,f))
|
|
self.sftp.rmdir(path)
|
|
else:
|
|
self.sftp.remove(path)
|
|
|
|
def _exec_cmdln(self, cmdln, procCtrl, stdinFile):
|
|
"""Execute an OS command line (as a single string) on the remote host.
|
|
cmdln - complete command line of the OS command
|
|
procCtrl - procCtrl object from message which controls how the process
|
|
is started (blocking vs non-blocking and output reporting)
|
|
"""
|
|
|
|
contents = None
|
|
if (stdinFile != None):
|
|
with self.open(stdinFile) as stdin:
|
|
contents = stdin.read()
|
|
|
|
with contextlib.closing(self.client.get_transport().open_session()) as chan:
|
|
chan.set_combine_stderr(True)
|
|
_logger.debug('cmdln='+cmdln)
|
|
chan.exec_command(cmdln)
|
|
|
|
if (contents != None):
|
|
_logger.debug('Using supplied stdin from ' + stdinFile + ': ')
|
|
_logger.debug(contents[0:50] + '...')
|
|
chan.sendall(contents)
|
|
chan.shutdown_write()
|
|
|
|
if util.get_val(procCtrl, 'waitForCompletion'):
|
|
output = chan.makefile('rb')
|
|
_logger.debug('Waiting for command...')
|
|
exitstatus = chan.recv_exit_status()
|
|
if exitstatus != 0 and exitstatus != util.get_val(procCtrl, 'noRaise'):
|
|
raise RemoteExecException(self.host, cmdln, exitstatus, output)
|
|
return output.read()
|
|
else:
|
|
if not chan.exit_status_ready() and procCtrl.has_key('daemonWait'):
|
|
_logger.debug('Waiting {0} sec for {1}'.format(procCtrl['daemonWait'], cmdln))
|
|
time.sleep(procCtrl['daemonWait'])
|
|
if chan.exit_status_ready():
|
|
output = chan.makefile('rb')
|
|
raise RemoteExecException(self.host, cmdln, chan.recv_exit_status(), output)
|
|
|
|
def _exec_cmdv(self, cmdv, procCtrl, stdinFile):
|
|
"""Execute an OS command vector on the remote host.
|
|
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)
|
|
"""
|
|
|
|
assert isinstance(cmdv, list)
|
|
return self._exec_cmdln(' '.join([quote_if_contains_space(a) for a in cmdv]), procCtrl, stdinFile)
|
|
|
|
|
|
def execute_command(self, cmdv, inFile=None):
|
|
"""Execute an OS command blocking on the local host, using
|
|
subprocess module. Returns dict contaning output from process.
|
|
cmdv - complete command vector (argv) of the OS command.
|
|
inFile - File-like object providing stdin to the command.
|
|
"""
|
|
cmdln = ' '.join([quote_if_contains_space(a) for a in cmdv])
|
|
_logger.debug('cmdln='+cmdln)
|
|
|
|
with contextlib.closing(self.client.get_transport().open_session()) as chan:
|
|
chan.exec_command(cmdln)
|
|
if inFile:
|
|
chan.sendall(inFile.read())
|
|
chan.shutdown_write()
|
|
|
|
result = {
|
|
'exitstatus': chan.recv_exit_status()
|
|
}
|
|
with contextlib.closing(chan.makefile('rb')) as outFile:
|
|
result['out'] = outFile.read()
|
|
|
|
with contextlib.closing(chan.makefile_stderr('rb')) as errFile:
|
|
result['err'] = errFile.read(),
|
|
|
|
return result
|
|
|