mysql5/mysql-5.7.27/storage/ndb/mcc/remote_clusterhost.py

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