mirror of
https://github.com/docker/docs.git
synced 2026-04-01 08:48:56 +07:00
441 lines
14 KiB
Python
441 lines
14 KiB
Python
#
|
|
# Copyright (C) 2016 Rolf Neugebauer <rolf.neugebauer@docker.com>
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
|
|
"""Main entry point to run the regression test on a (remote) host"""
|
|
|
|
import abc
|
|
import argparse
|
|
import base64
|
|
import fnmatch
|
|
import os
|
|
import socket
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import threading
|
|
|
|
import rt.httpsvr
|
|
import rt.log
|
|
import winrm
|
|
|
|
|
|
# Temporary directories on the remote hosts
|
|
_WIN_TMP_DIR = "/c/Users/%s/AppData/Local/RT"
|
|
_OSX_TMP_DIR = "/tmp/RT"
|
|
|
|
# globals for ports to use as well as IP address to pass to remotes
|
|
_PORT_AUX_HTTP = -1
|
|
_PORT_AUX_LOG = -1
|
|
_LOCAL_IP = None
|
|
|
|
|
|
def _targz_dir(dst, directory, excludes=None):
|
|
"""tar up a directory"""
|
|
tf = tarfile.open(dst, mode="w:gz")
|
|
for root, _, files in os.walk(directory):
|
|
for filename in files:
|
|
fp = os.path.join(root, filename)
|
|
if any(fnmatch.fnmatch(fp, pat) for pat in excludes):
|
|
continue
|
|
tf.add(fp)
|
|
tf.close()
|
|
|
|
|
|
class Unbuffered(object):
|
|
"""
|
|
This class is used to make sure stdout is not buffered
|
|
"""
|
|
def __init__(self, stream):
|
|
self.stream = stream
|
|
|
|
def write(self, data):
|
|
self.stream.write(data)
|
|
self.stream.flush()
|
|
|
|
def __getattr__(self, attr):
|
|
return getattr(self.stream, attr)
|
|
|
|
|
|
class _Host:
|
|
"""An abstract base class to interact with a host"""
|
|
__metaclass__ = abc.ABCMeta
|
|
|
|
def __init__(self, remote, user, password):
|
|
"""Initialise the class"""
|
|
self.remote = remote
|
|
self.user = user
|
|
self.password = password
|
|
return
|
|
|
|
@abc.abstractmethod
|
|
def run_sh(self, cmd):
|
|
"""Run a shell command on Host"""
|
|
pass
|
|
|
|
def cp_to(self, lpath, rpath, excludes=None):
|
|
"""Copy a file from the local host to the remote host. If @lpath ends
|
|
with '/' copy the directory recursively. @rpath *must* be a
|
|
directory. Optionally supply a list of glob expressions for
|
|
files to ignore."""
|
|
print("COPY TO: %s -> %s" % (lpath, rpath))
|
|
if not lpath.endswith('/'):
|
|
self._cp_to(lpath, rpath)
|
|
# we have a directory to copy.
|
|
# 1. Tar up directory to a temporary file
|
|
fd, fp = tempfile.mkstemp(suffix=".tar.gz")
|
|
_targz_dir(fp, lpath, excludes)
|
|
_, fn = os.path.split(fp)
|
|
|
|
# 2. Copy file to the host and remove tmp file
|
|
rc = self._cp_to(fp, rpath)
|
|
os.close(fd)
|
|
os.remove(fp)
|
|
if not rc == 0:
|
|
return rc
|
|
|
|
# 3. Untar the file on the host
|
|
rc = self.run_sh("(cd %s; tar xzf %s; rm %s)" %
|
|
(rpath, fn, fn))
|
|
return rc
|
|
|
|
@abc.abstractmethod
|
|
def _cp_to(self, lpath, rpath):
|
|
""" Class specfic implementation of a copy to operation"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def cp_from(self, rpath, lpath):
|
|
"""Copy a directory from the remote host to the local host. If @rpath
|
|
ends with a '/' copy the directory recursively. @lpath *must*
|
|
be a directory."""
|
|
pass
|
|
|
|
def run_rt_local(self, rpath, args):
|
|
"""Run tests on remote system. This is special method as it allows
|
|
different backends to modify the arguments a little if
|
|
needed. @rpath is where 'rt-local' can be found"""
|
|
|
|
rc = self.run_sh("cd %s; python ./rt-local %s" % (rpath, args))
|
|
return rc
|
|
|
|
|
|
class _LogThread(threading.Thread):
|
|
"""Listen on a logging socket and print stuff till close()"""
|
|
|
|
def __init__(self, port):
|
|
threading.Thread.__init__(self)
|
|
self.port = port
|
|
|
|
def run(self):
|
|
rt.log.log_svr(self.port)
|
|
|
|
|
|
class WinRMHost(_Host):
|
|
"""A class to interact with remote host via Windows Remote Management"""
|
|
# This is fun.
|
|
# - WinRM does not allow one to send file to and fro. So we use
|
|
# WinRM to execute bash commands on host and use a local web
|
|
# server to handle GET/POST request to transfer file. When
|
|
# transferring directories we tar them before transferring.
|
|
# - WinRM also only gives you the output of a command once it is
|
|
# done. So, we log over a socket and have a log server running
|
|
# locally to print the logs as they come in.
|
|
# - pywinrm chokes when a command runs to long, so never returns.
|
|
# So we start in the background and exit once the log thread exits.
|
|
|
|
def __init__(self, remote, user, password):
|
|
"""Initialise the class"""
|
|
super(WinRMHost, self).__init__(remote, user, password)
|
|
|
|
self.our_ip = _LOCAL_IP if _LOCAL_IP else self.get_local_ip(remote)
|
|
|
|
self.session = winrm.Session("%s" % remote,
|
|
auth=(self.user, self.password),
|
|
server_cert_validation='ignore')
|
|
|
|
@staticmethod
|
|
def get_local_ip(remote):
|
|
"""Try to get local IP address used to connect to the remote host"""
|
|
try:
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
s.connect((remote, 53))
|
|
our_ip = s.getsockname()[0]
|
|
s.close()
|
|
except:
|
|
our_ip = socket.gethostbyname(socket.gethostname())
|
|
return our_ip
|
|
|
|
def run_sh(self, cmd):
|
|
"""Run a shell command on the host"""
|
|
|
|
# This is really, really tedious, but I could not get cmd.exe
|
|
# to run bash directly, so we start powershell and run it from
|
|
# there. Further, winrm.run_ps does not work either.
|
|
s = self.session
|
|
print("EXEC: %s" % cmd)
|
|
rs = s.run_cmd("powershell.exe",
|
|
['-NonInteractive',
|
|
'-ExecutionPolicy', 'Unrestricted',
|
|
'-Command',
|
|
"\"& 'C:\\Program Files\\Git\\bin\\bash.exe' " +
|
|
"--login -c '%s' \"" % cmd])
|
|
if len(rs.std_out.strip()):
|
|
print("STDOUT:")
|
|
for l in rs.std_out.split('\n'):
|
|
print(l)
|
|
if len(rs.std_err.strip()):
|
|
print("STDERR:")
|
|
for l in rs.std_err.split('\n'):
|
|
print(l)
|
|
return rs.status_code
|
|
|
|
def _cp_to(self, lpath, rpath):
|
|
"""Copy a file in @lpath to the @rpath directory on the host."""
|
|
_, fn = os.path.split(lpath)
|
|
# 1. Start web server with file
|
|
s = rt.httpsvr.dl_svr_start(_PORT_AUX_HTTP, lpath)
|
|
# 2. Execute curl (after making sure the dir exist)
|
|
self.run_sh("mkdir -p %s" % rpath)
|
|
rc = self.run_sh("(cd %s; curl -s %s:%d -o %s)" %
|
|
(rpath, self.our_ip, _PORT_AUX_HTTP, fn))
|
|
# 3. Make sure server is stopped
|
|
rt.httpsvr.dl_svr_wait(s)
|
|
return rc
|
|
|
|
def _cp_from(self, rpath, lpath):
|
|
"""Copy a file from @rpath to the local @lpath directory."""
|
|
rc = subprocess.call("mkdir -p %s" % lpath, shell=True)
|
|
fp, fn = os.path.split(rpath)
|
|
# 1. Start web server to write a POST to the file
|
|
s = rt.httpsvr.ul_svr_start(_PORT_AUX_HTTP, os.path.join(lpath, fn))
|
|
# 2. Execute curl
|
|
rc = self.run_sh("(cd %s; curl -s -X POST --data-binary @%s %s:%d)" %
|
|
(fp, fn, self.our_ip, _PORT_AUX_HTTP))
|
|
# 3. Make sure server is stopped
|
|
rt.httpsvr.ul_svr_wait(s)
|
|
return rc
|
|
|
|
def cp_from(self, rpath, lpath):
|
|
print("COPY FROM: %s -> %s" % (rpath, lpath))
|
|
if not rpath.endswith('/'):
|
|
self._cp_from(rpath, lpath)
|
|
|
|
# local temporary file (only use it for the filename)
|
|
fd, lfp = tempfile.mkstemp(suffix=".tar.gz")
|
|
os.close(fd)
|
|
_, fn = os.path.split(lfp)
|
|
|
|
# we have a directory to copy.
|
|
# 1. Tar up directory to a temporary file
|
|
rfn = os.path.join("/c/Users/%s/AppData/Local/" % self.user, fn)
|
|
print(rfn)
|
|
# get parent directory and directory to tar
|
|
rc = self.run_sh("(cd %s; tar czf %s .)" %
|
|
(rpath, rfn))
|
|
if not rc == 0:
|
|
return rc
|
|
|
|
# 2. Copy file to the host and delete the remote file
|
|
rc = self._cp_from(rfn, lpath)
|
|
self.run_sh("rm -f %s" % rfn)
|
|
if not rc == 0:
|
|
return rc
|
|
|
|
# 3. Untar and remove local tar file
|
|
rc = subprocess.call("(cd %s; tar xzf %s)" % (lpath, fn), shell=True)
|
|
os.remove(os.path.join(lpath, fn))
|
|
return rc
|
|
|
|
def run_rt_local(self, rpath, args):
|
|
# When running tests pipe output to /dev/null. We can only get
|
|
# it after the command was run anyway so it's a bit
|
|
# useless. Instead we run a Logging server in a thread and
|
|
# configure rt-local to log to it.
|
|
bg = False
|
|
if " run" in args:
|
|
thd = _LogThread(_PORT_AUX_LOG)
|
|
thd.start()
|
|
args = "--logger %s:%d " % (self.our_ip, _PORT_AUX_LOG) + args
|
|
bg = True
|
|
|
|
# base64 encode the rt-local arguments in case of special chars
|
|
rc = _Host.run_rt_local(self, rpath, "%s %s" %
|
|
(base64.b64encode(args),
|
|
" > /dev/null 2> /dev/null &" if bg else ""))
|
|
|
|
if " run" in args:
|
|
print("'rt-local' started in background on remote host")
|
|
thd.join()
|
|
# XXX ToDo: reflect the rt-local return value here
|
|
return rc
|
|
|
|
|
|
class SSHHost(_Host):
|
|
"""A class to interact with remote host via SSH"""
|
|
|
|
def __init__(self, remote, user, password):
|
|
super(SSHHost, self).__init__(remote, user, password)
|
|
self.connection_string = "%s@%s" % (self.user, self.remote)
|
|
self.key_file = os.path.join(os.getcwd(), "etc", "ssh", "id_rsa")
|
|
self.ignore_host_key = '-oStrictHostKeyChecking=no'
|
|
self.known_hosts_file = '-oUserKnownHostsFile=/dev/null'
|
|
|
|
# Check SSH key has correcy permissions
|
|
perms = stat.S_IRUSR & stat.S_IWUSR
|
|
st = os.stat(self.key_file)
|
|
current_perms = stat.S_IMODE(st.st_mode)
|
|
if current_perms != perms:
|
|
os.chmod(self.key_file, 0o600)
|
|
|
|
def run_sh(self, cmd):
|
|
"""Run a shell command on the host"""
|
|
print("EXEC: %s" % cmd)
|
|
ssh_cmd = [
|
|
"ssh",
|
|
"-q",
|
|
self.ignore_host_key,
|
|
self.known_hosts_file,
|
|
"-i",
|
|
self.key_file,
|
|
self.connection_string,
|
|
'%s' % (cmd)
|
|
]
|
|
# Use Popen to avoid jamming the stdout buffer
|
|
process = subprocess.Popen(ssh_cmd,
|
|
stdin=None,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
for line in iter(process.stdout.readline, ''):
|
|
sys.stdout.write(line)
|
|
|
|
process.communicate()
|
|
return process.returncode
|
|
|
|
def cp_from(self, rpath, lpath):
|
|
print("COPY FROM: %s -> %s" % (rpath, lpath))
|
|
if not rpath.endswith('/'):
|
|
rc = self.scp_from(False, rpath, lpath)
|
|
else:
|
|
rc = self.scp_from(True, rpath, lpath)
|
|
return rc
|
|
|
|
def _cp_to(self, lpath, rpath):
|
|
print("COPY TO: %s -> %s" % (lpath, rpath))
|
|
scp_cmd = [
|
|
"scp",
|
|
"-q",
|
|
self.ignore_host_key,
|
|
"-i",
|
|
self.key_file,
|
|
lpath,
|
|
"%s:%s" % (self.connection_string, rpath)
|
|
]
|
|
code = subprocess.call(scp_cmd)
|
|
return code
|
|
|
|
def scp_from(self, is_dir, rpath, lpath):
|
|
print("COPY FROM: %s -> %s" % (rpath, lpath))
|
|
if is_dir:
|
|
scp = ["scp", "-q", "-r"]
|
|
if not rpath.endswith("*"):
|
|
rpath = rpath + "*"
|
|
else:
|
|
scp = ["scp", "-q"]
|
|
|
|
scp_cmd = scp + [
|
|
self.ignore_host_key,
|
|
"-i",
|
|
self.key_file,
|
|
"%s:%s" % (self.connection_string, rpath),
|
|
lpath
|
|
]
|
|
code = subprocess.call(scp_cmd)
|
|
return code
|
|
|
|
|
|
def main():
|
|
"""Main entry point to run regressions tests on a (remote) host"""
|
|
|
|
# Make sure stdout is not buffered
|
|
sys.stdout = Unbuffered(sys.stdout)
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.description = \
|
|
"Execute rt-local commands on a (remote) host"
|
|
parser.epilog = \
|
|
"Arguments after '--' are passed to rt-local. " + \
|
|
"If no '--' is provided only the tests are copied " + \
|
|
"over to the remote system."
|
|
|
|
parser.add_argument('-t', '--type', nargs=1, required=True, dest='rtype',
|
|
choices=['osx', 'win'],
|
|
help="Type of host")
|
|
|
|
parser.add_argument("-r", "--remote", required=True,
|
|
help="Remote host IP or name")
|
|
|
|
parser.add_argument("--portbase", type=int, default=12000,
|
|
help="Port(s) to use for auxiliary services")
|
|
|
|
parser.add_argument("--local-ip", type=str, dest='localip',
|
|
help="Override local IP clients connect to.")
|
|
|
|
parser.add_argument("-u", "--user", default="docker",
|
|
help="Username to use")
|
|
|
|
parser.add_argument("-p", "--password", default="Pass30rd!",
|
|
help="Password for user")
|
|
|
|
# Split argv at -- and parse
|
|
host_args, _, local_args = " ".join(sys.argv).partition(" -- ")
|
|
args = parser.parse_args(host_args.split()[1:])
|
|
|
|
global _PORT_AUX_HTTP
|
|
global _PORT_AUX_LOG
|
|
global _LOCAL_IP
|
|
_PORT_AUX_HTTP = args.portbase
|
|
_PORT_AUX_LOG = args.portbase + 1
|
|
_LOCAL_IP = args.localip
|
|
|
|
if args.rtype[0] == "osx":
|
|
h = SSHHost(args.remote, args.user, args.password)
|
|
remote_tmp_dir = _OSX_TMP_DIR
|
|
elif args.rtype[0] == "win":
|
|
h = WinRMHost(args.remote, args.user, args.password)
|
|
remote_tmp_dir = _WIN_TMP_DIR % args.user
|
|
|
|
# prepare system
|
|
print("\n=== Clean Remote system")
|
|
h.run_sh("rm -rf %s" % remote_tmp_dir)
|
|
h.run_sh("mkdir %s" % remote_tmp_dir)
|
|
|
|
print("\n=== Copy infrastructure and test cases")
|
|
# XXX Should really parse local_args for `-c` and copy them
|
|
# separate if specified.
|
|
h.cp_to("./", remote_tmp_dir, ["./_results/*",
|
|
"*.pyc",
|
|
"./cases/_tmp/*",
|
|
"./etc/*"])
|
|
|
|
if len(local_args) == 0:
|
|
return
|
|
|
|
print("\n=== Run 'rt-local' on remote host")
|
|
rc = h.run_rt_local(remote_tmp_dir, local_args)
|
|
|
|
if " run" in local_args:
|
|
print("\n=== Copy Results")
|
|
t = h.cp_from(remote_tmp_dir + "/_results/", "./_results")
|
|
rc = t if rc == 0 else rc
|
|
|
|
return rc
|