Add initial version of autoserv
git-svn-id: http://test.kernel.org/svn/autotest/trunk@557 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/server/hosts/ssh_host.py b/server/hosts/ssh_host.py
new file mode 100644
index 0000000..eb4a301
--- /dev/null
+++ b/server/hosts/ssh_host.py
@@ -0,0 +1,301 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc. Released under the GPL v2
+
+"""This module defines the SSHHost class.
+
+Implementation details:
+You should import the "hosts" package instead of importing each type of host.
+
+ SSHHost: a remote machine with a ssh access
+"""
+
+__author__ = """mbligh@google.com (Martin J. Bligh),
+poirier@google.com (Benjamin Poirier),
+stutsman@google.com (Ryan Stutsman)"""
+
+
+import types
+import os
+import time
+
+import base_classes
+import utils
+import errors
+
+
+class SSHHost(base_classes.RemoteHost):
+ """This class represents a remote machine controlled through an ssh
+ session on which you can run programs.
+
+ It is not the machine autoserv is running on. The machine must be
+ configured for password-less login, for example through public key
+ authentication.
+
+ Implementation details:
+ This is a leaf class in an abstract class hierarchy, it must
+ implement the unimplemented methods in parent classes.
+ """
+
+ def __init__(self, hostname, user="root", port=22):
+ """Construct a SSHHost object
+
+ Args:
+ hostname: network hostname or address of remote machine
+ user: user to log in as on the remote machine
+ port: port the ssh daemon is listening on on the remote
+ machine
+ """
+ super(SSHHost, self).__init__()
+
+ self.hostname= hostname
+ self.user= user
+ self.port= port
+ self.tmp_dirs= []
+
+ def __del__(self):
+ """Destroy a SSHHost object
+ """
+ for dir in self.tmp_dirs:
+ try:
+ self.run('rm -rf "%s"' % (utils.sh_escape(dir)))
+ except errors.AutoservRunError:
+ pass
+
+ def run(self, command, timeout=None):
+ """Run a command on the remote host.
+
+ Args:
+ command: the command line string
+ timeout: time limit in seconds before attempting to
+ kill the running process. The run() function
+ will take a few seconds longer than 'timeout'
+ to complete if it has to kill the process.
+
+ Returns:
+ a hosts.base_classes.CmdResult object
+
+ Raises:
+ AutoservRunError: the exit code of the command
+ execution was not 0
+ """
+ #~ print "running %s" % (command,)
+ result= utils.run(r'ssh -l %s -p %d %s "%s"' % (self.user,
+ self.port, self.hostname, utils.sh_escape(command)),
+ timeout)
+ return result
+
+ def reboot(self):
+ """Reboot the remote host.
+
+ TODO(poirier): Should the function return only after having
+ done a self.wait_down()? or should this be left to
+ the control file?
+ pro: A common usage pattern would be reboot(),
+ wait_down(), wait_up(), [more commands]. If wait_down()
+ is not there, wait_up() is likely to return right away
+ because the ssh daemon has not yet shutdown, so a
+ control file expecting the host to have rebooted will
+ run eronously. Doing the wait_down() in reboot
+ eliminates the risk of confusion. Also, making the
+ wait_down() external might lead to race conditions if
+ the control file does a reboot() does some other things,
+ then there's no way to know if it should wait_down()
+ first or wait_up() right away.
+ con: wait_down() just after reboot will be mandatory,
+ this might be undesirable if there are other operations
+ that can be executed right after the reboot, for
+ example many hosts have to be rebooted at the same
+ time. The solution to this is to use multiple
+ threads of execution in the control file.
+ """
+ self.run("reboot")
+ self.wait_down()
+
+ def get_file(self, source, dest):
+ """Copy files from the remote host to a local path.
+
+ Directories will be copied recursively.
+ If a source component is a directory with a trailing slash,
+ the content of the directory will be copied, otherwise, the
+ directory itself and its content will be copied. This
+ behavior is similar to that of the program 'rsync'.
+
+ Args:
+ source: either
+ 1) a single file or directory, as a string
+ 2) a list of one or more (possibly mixed)
+ files or directories
+ dest: a file or a directory (if source contains a
+ directory or more than one element, you must
+ supply a directory dest)
+
+ Raises:
+ AutoservRunError: the scp command failed
+ """
+ if isinstance(source, types.StringTypes):
+ source= [source]
+
+ processed_source= []
+ for entry in source:
+ if entry.endswith('/'):
+ format_string= '%s@%s:"%s*"'
+ else:
+ format_string= '%s@%s:"%s"'
+ entry= format_string % (self.user, self.hostname,
+ utils.scp_remote_escape(entry))
+ processed_source.append(entry)
+
+ processed_dest= os.path.abspath(dest)
+ if os.path.isdir(dest):
+ processed_dest= "%s/" % (utils.sh_escape(processed_dest),)
+ else:
+ processed_dest= utils.sh_escape(processed_dest)
+
+ utils.run('scp -rpq %s "%s"' % (
+ " ".join(processed_source),
+ processed_dest))
+
+ def send_file(self, source, dest):
+ """Copy files from a local path to the remote host.
+
+ Directories will be copied recursively.
+ If a source component is a directory with a trailing slash,
+ the content of the directory will be copied, otherwise, the
+ directory itself and its content will be copied. This
+ behavior is similar to that of the program 'rsync'.
+
+ Args:
+ source: either
+ 1) a single file or directory, as a string
+ 2) a list of one or more (possibly mixed)
+ files or directories
+ dest: a file or a directory (if source contains a
+ directory or more than one element, you must
+ supply a directory dest)
+
+ Raises:
+ AutoservRunError: the scp command failed
+ """
+ if isinstance(source, types.StringTypes):
+ source= [source]
+
+ processed_source= []
+ for entry in source:
+ if entry.endswith('/'):
+ format_string= '"%s/"*'
+ else:
+ format_string= '"%s"'
+ entry= format_string % (utils.sh_escape(os.path.abspath(entry)),)
+ processed_source.append(entry)
+
+ utils.run('scp -rpq %s %s@%s:"%s"' % (
+ " ".join(processed_source), self.user, self.hostname,
+ utils.scp_remote_escape(dest)))
+
+ def get_tmp_dir(self):
+ """Return the pathname of a directory on the host suitable
+ for temporary file storage.
+
+ The directory and its content will be deleted automatically
+ on the destruction of the Host object that was used to obtain
+ it.
+ """
+ dir_name= self.run("mktemp -dt autoserv-XXXXXX").stdout.rstrip(" \n")
+ self.tmp_dirs.append(dir_name)
+ return dir_name
+
+ def is_up(self):
+ """Check if the remote host is up.
+
+ Returns:
+ True if the remote host is up, False otherwise
+ """
+ try:
+ result= self.run("true", timeout=10)
+ except errors.AutoservRunError:
+ return False
+ else:
+ if result.exit_status == 0:
+ return True
+ else:
+ return False
+
+ def wait_up(self, timeout=None):
+ """Wait until the remote host is up or the timeout expires.
+
+ In fact, it will wait until an ssh connection to the remote
+ host can be established.
+
+ Args:
+ timeout: time limit in seconds before returning even
+ if the host is not up.
+
+ Returns:
+ True if the host was found to be up, False otherwise
+ """
+ if timeout:
+ end_time= time.time() + timeout
+
+ while not timeout or time.time() < end_time:
+ try:
+ if timeout:
+ run_timeout= end_time - time.time()
+ else:
+ run_timeout= 10
+ result= self.run("true", timeout=run_timeout)
+ except errors.AutoservRunError:
+ pass
+ else:
+ if result.exit_status == 0:
+ return True
+ time.sleep(1)
+
+ return False
+
+ def wait_down(self, timeout=None):
+ """Wait until the remote host is down or the timeout expires.
+
+ In fact, it will wait until an ssh connection to the remote
+ host fails.
+
+ Args:
+ timeout: time limit in seconds before returning even
+ if the host is not up.
+
+ Returns:
+ True if the host was found to be down, False otherwise
+ """
+ if timeout:
+ end_time= time.time() + timeout
+
+ while not timeout or time.time() < end_time:
+ try:
+ if timeout:
+ run_timeout= end_time - time.time()
+ else:
+ run_timeout= 10
+ result= self.run("true", timeout=run_timeout)
+ except errors.AutoservRunError:
+ return True
+ else:
+ if result.aborted:
+ return True
+ time.sleep(1)
+
+ return False
+
+ def get_num_cpu(self):
+ """Get the number of CPUs in the host according to
+ /proc/cpuinfo.
+
+ Returns:
+ The number of CPUs
+ """
+
+ proc_cpuinfo= self.run("cat /proc/cpuinfo").stdout
+ cpus = 0
+ for line in proc_cpuinfo.splitlines():
+ if line.startswith('processor'):
+ cpus += 1
+ return cpus