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/__init__.py b/server/hosts/__init__.py
new file mode 100644
index 0000000..1b40526
--- /dev/null
+++ b/server/hosts/__init__.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc. Released under the GPL v2
+
+"""This is a convenience module to import all available types of hosts.
+
+Implementation details:
+You should 'import hosts' instead of importing every available host module.
+"""
+
+__author__ = """mbligh@google.com (Martin J. Bligh),
+poirier@google.com (Benjamin Poirier),
+stutsman@google.com (Ryan Stutsman)"""
+
+
+# host abstract classes
+from base_classes import Host
+from base_classes import SiteHost
+from base_classes import RemoteHost
+
+# host implementation classes
+from ssh_host import SSHHost
+from conmux_ssh_host import ConmuxSSHHost
+from guest import Guest
+from kvm_guest import KVMGuest
+
+# bootloader classes
+from bootloader import Bootloader
+from lilo import Lilo
+from grub import Grub
+
+# command result class
+from base_classes import CmdResult
diff --git a/server/hosts/base_classes.py b/server/hosts/base_classes.py
new file mode 100644
index 0000000..dac153a
--- /dev/null
+++ b/server/hosts/base_classes.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc. Released under the GPL v2
+
+"""This module defines the base classes for the Host hierarchy.
+
+Implementation details:
+You should import the "hosts" package instead of importing each type of host.
+
+ Host: a machine on which you can run programs
+ RemoteHost: a remote machine on which you can run programs
+ CmdResult: contain the results of a Host.run() command execution
+"""
+
+__author__ = """mbligh@google.com (Martin J. Bligh),
+poirier@google.com (Benjamin Poirier),
+stutsman@google.com (Ryan Stutsman)"""
+
+
+import time
+import textwrap
+
+
+class Host(object):
+ """This class represents a machine on which you can run programs.
+
+ It may be a local machine, the one autoserv is running on, a remote
+ machine or a virtual machine.
+
+ Implementation details:
+ This is an abstract class, leaf subclasses must implement the methods
+ listed here. You must not instantiate this class but should
+ instantiate one of those leaf subclasses."""
+
+ bootloader = None
+
+ def __init__(self):
+ super(Host, self).__init__()
+
+ def run(self, command):
+ pass
+
+ def reboot(self):
+ pass
+
+ def get_file(self, source, dest):
+ pass
+
+ def send_file(self, source, dest):
+ pass
+
+ def get_tmp_dir(self):
+ pass
+
+ def is_up(self):
+ pass
+
+ def wait_up(self, timeout):
+ pass
+
+ def wait_down(self, timeout):
+ pass
+
+ def get_num_cpu(self):
+ pass
+
+ def install(self, installableObject):
+ installableObject.install(self)
+
+
+# site_host.py may be non-existant or empty, make sure that an appropriate
+# SiteHost class is created nevertheless
+try:
+ from site_host import SiteHost
+except ImportError:
+ pass
+
+if not "SiteHost" in dir():
+ class SiteHost(Host):
+ def __init__(self):
+ super(SiteHost, self).__init__()
+
+
+class RemoteHost(SiteHost):
+ """This class represents a remote machine on which you can run
+ programs.
+
+ It may be accessed through a network, a serial line, ...
+ It is not the machine autoserv is running on.
+
+ Implementation details:
+ This is an abstract class, leaf subclasses must implement the methods
+ listed here and in parent classes which have no implementation. They
+ may reimplement methods which already have an implementation. You
+ must not instantiate this class but should instantiate one of those
+ leaf subclasses."""
+
+ hostname= None
+
+ def __init__(self):
+ super(RemoteHost, self).__init__()
+
+
+class CmdResult(object):
+ """
+ Command execution result.
+
+ Modified from the original Autoserv code, local_cmd.py:
+ Copyright jonmayer@google.com (Jonathan Mayer),
+ mbligh@google.com (Martin J. Bligh)
+ Released under the GPL, v2
+
+ command: String containing the command line itself
+ exit_status: Integer exit code of the process
+ stdout: String containing stdout of the process
+ stderr: String containing stderr of the process
+ duration: Elapsed wall clock time running the process
+ aborted: Signal that caused the command to terminate (0 if none)
+ """
+
+ def __init__(self):
+ super(CmdResult, self).__init__()
+ self.command = ""
+ self.exit_status = None
+ self.stdout = ""
+ self.stderr = ""
+ self.duration = 0
+ self.aborted= False
+
+ def __repr__(self):
+ wrapper= textwrap.TextWrapper(width=78,
+ initial_indent="\n ", subsequent_indent=" ")
+
+ stdout= self.stdout.rstrip(" \n")
+ if stdout:
+ stdout= "\nStdout:\n%s" % (stdout,)
+
+ stderr= self.stderr.rstrip(" \n")
+ if stderr:
+ stderr= "\nStderr:\n%s" % (stderr,)
+
+ return ("* Command: %s\n"
+ "Exit status: %s\n"
+ "Duration: %s\n"
+ "Aborted: %s"
+ "%s"
+ "%s"
+ % (wrapper.fill(self.command), self.exit_status,
+ self.duration, self.aborted, stdout, stderr))
diff --git a/server/hosts/bootloader.py b/server/hosts/bootloader.py
new file mode 100644
index 0000000..eeaa5fb
--- /dev/null
+++ b/server/hosts/bootloader.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc. Released under the GPL v2
+
+"""This module defines the Bootloader class.
+
+ Bootloader: a program to boot Kernels on a Host.
+"""
+
+__author__ = """mbligh@google.com (Martin J. Bligh),
+poirier@google.com (Benjamin Poirier),
+stutsman@google.com (Ryan Stutsman)"""
+
+
+class Bootloader(object):
+ """This class represents a bootloader.
+
+ It can be used to add a kernel to the list of kernels that can be
+ booted by a bootloader. It can also make sure that this kernel will
+ be the one chosen at next reboot.
+
+ Implementation details:
+ This is an abstract class, leaf subclasses must implement the methods
+ listed here. You must not instantiate this class but should
+ instantiate one of those leaf subclasses."""
+
+ host = None
+
+ def add_entry(self, name, image, initrd, root, options, default=True):
+ pass
+
+ def remove_entry(self, name):
+ pass
diff --git a/server/hosts/conmux_ssh_host.py b/server/hosts/conmux_ssh_host.py
new file mode 100644
index 0000000..0cf1ad3
--- /dev/null
+++ b/server/hosts/conmux_ssh_host.py
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc. Released under the GPL v2
+
+"""This module defines the ConmuxSSHHost
+
+Implementation details:
+You should import the "hosts" package instead of importing each type of host.
+
+ ConmuxSSHHost: a remote machine controlled through a serial console
+"""
+
+__author__ = """mbligh@google.com (Martin J. Bligh),
+poirier@google.com (Benjamin Poirier),
+stutsman@google.com (Ryan Stutsman)"""
+
+import os
+import os.path
+import signal
+import subprocess
+
+import ssh_host
+
+class ConmuxSSHHost(ssh_host.SSHHost):
+ """This class represents a remote machine controlled through a serial
+ console on which you can run programs. It is not the machine autoserv
+ is running on.
+
+ For a machine controlled in this way, it may be possible to support
+ hard reset, boot strap monitoring or other operations not possible
+ on a machine controlled through ssh, telnet, ..."""
+
+ def __init__(self,
+ hostname,
+ logfilename=None,
+ server='localhost',
+ attach='/usr/local/conmux/bin/conmux-attach'):
+ super(ConmuxSSHHost, self).__init__(hostname)
+ self.server = server
+ self.attach = attach
+ self.pid = None
+ self.__start_console_log(logfilename)
+
+
+ def hardreset(self):
+ """
+ Reach out and slap the box in the power switch
+ """
+ self.__console_run(r"'~$hardreset'")
+
+
+ def __start_console_log(self, logfilename):
+ """
+ Log the output of the console session to a specified file
+ """
+ if not os.path.exists(self.attach):
+ return
+ cmd = [self.attach, '%s/%s' % (self.server, self.hostname), \
+ 'cat - > %s' % logfilename ]
+ self.pid = subprocess.Popen(cmd).pid
+
+
+ def __console_run(self, cmd):
+ """
+ Send a command to the conmux session
+ """
+ cmd = '%s %s/%s echo %s' % (self.attach,
+ self.server,
+ self.hostname,
+ cmd)
+ os.system(cmd)
+
+
+ def __del__(self):
+ if self.pid:
+ os.kill(self.pid, signal.SIGTERM)
+ super(ConmuxSSHHost, self).__del__()
+
diff --git a/server/hosts/grub.py b/server/hosts/grub.py
new file mode 100644
index 0000000..39ab0dd
--- /dev/null
+++ b/server/hosts/grub.py
@@ -0,0 +1,34 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc. Released under the GPL v2
+
+"""This module defines the Grub class.
+
+Implementation details:
+You should import the "hosts" package instead of importing this module directly.
+
+ Grub: a particular Bootloader
+"""
+
+__author__ = """mbligh@google.com (Martin J. Bligh),
+poirier@google.com (Benjamin Poirier),
+stutsman@google.com (Ryan Stutsman)"""
+
+
+import bootloader
+
+
+class Grub(bootloader.Bootloader):
+ """This class represents the GRUB bootloader.
+
+ It can be used to add a kernel to the list of kernels that can be
+ booted. It can also make sure that this kernel will be the one
+ chosen at next reboot.
+
+ Implementation details:
+ This is a leaf class in an abstract class hierarchy, it must
+ implement the unimplemented methods in parent classes.
+ """
+
+ pass
+
diff --git a/server/hosts/guest.py b/server/hosts/guest.py
new file mode 100644
index 0000000..9f4d89c
--- /dev/null
+++ b/server/hosts/guest.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc. Released under the GPL v2
+
+"""This module defines the Guest class in the Host hierarchy.
+
+Implementation details:
+You should import the "hosts" package instead of importing each type of host.
+
+ Guest: a virtual machine on which you can run programs
+"""
+
+__author__ = """mbligh@google.com (Martin J. Bligh),
+poirier@google.com (Benjamin Poirier),
+stutsman@google.com (Ryan Stutsman)"""
+
+
+import ssh_host
+
+
+class Guest(ssh_host.SSHHost):
+ """This class represents a virtual machine on which you can run
+ programs.
+
+ It is not the machine autoserv is running on.
+
+ Implementation details:
+ This is an abstract class, leaf subclasses must implement the methods
+ listed here and in parent classes which have no implementation. They
+ may reimplement methods which already have an implementation. You
+ must not instantiate this class but should instantiate one of those
+ leaf subclasses."""
+
+ controllingHypervisor = None
+
+ def __init__(self, controllingHypervisor):
+ """Construct a Guest object
+
+ Args:
+ controllingHypervisor: Hypervisor object that is
+ responsible for the creation and management of this
+ guest
+ hostname: network hostname or address of virtual machine
+ """
+ hostname= controllingHypervisor.new_guest()
+ super(Guest, self).__init__(hostname)
+ self.controllingHypervisor= controllingHypervisor
+
+ def __del__(self):
+ """Destroy a Guest object
+ """
+ self.controllingHypervisor.delete_guest(self.hostname)
+
+ def hardreset(self):
+ """Perform a "hardreset" of the guest.
+
+ It is restarted through the hypervisor. That will restart it
+ even if the guest otherwise innaccessible through ssh.
+ """
+ return self.controllingHypervisor.reset_guest(self.name)
+
diff --git a/server/hosts/kvm_guest.py b/server/hosts/kvm_guest.py
new file mode 100644
index 0000000..ca120f9
--- /dev/null
+++ b/server/hosts/kvm_guest.py
@@ -0,0 +1,30 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc. Released under the GPL v2
+
+"""This module defines the Host class.
+
+Implementation details:
+You should import the "hosts" package instead of importing each type of host.
+
+ KVMGuest: a KVM virtual machine on which you can run programs
+"""
+
+__author__ = """mbligh@google.com (Martin J. Bligh),
+poirier@google.com (Benjamin Poirier),
+stutsman@google.com (Ryan Stutsman)"""
+
+
+import guest
+
+
+class KVMGuest(guest.Guest):
+ """This class represents a KVM virtual machine on which you can run
+ programs.
+
+ Implementation details:
+ This is a leaf class in an abstract class hierarchy, it must
+ implement the unimplemented methods in parent classes.
+ """
+
+ pass
diff --git a/server/hosts/lilo.py b/server/hosts/lilo.py
new file mode 100644
index 0000000..9560384
--- /dev/null
+++ b/server/hosts/lilo.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc. Released under the GPL v2
+
+"""This module defines the Lilo class.
+
+Implementation details:
+You should import the "hosts" package instead of importing this module directly.
+
+ Lilo: a particular Bootloader
+"""
+
+__author__ = """mbligh@google.com (Martin J. Bligh),
+poirier@google.com (Benjamin Poirier),
+stutsman@google.com (Ryan Stutsman)"""
+
+
+import bootloader
+
+
+class Lilo(bootloader.Bootloader):
+ """This class represents the Lilo bootloader.
+
+ It can be used to add a kernel to the list of kernels that can be
+ booted. It can also make sure that this kernel will be the one
+ chosen at next reboot.
+
+ Implementation details:
+ This is a leaf class in an abstract class hierarchy, it must
+ implement the unimplemented methods in parent classes.
+ """
+
+ pass
diff --git a/server/hosts/site_host.py b/server/hosts/site_host.py
new file mode 100644
index 0000000..d5ebfc7
--- /dev/null
+++ b/server/hosts/site_host.py
@@ -0,0 +1,35 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc. Released under the GPL v2
+
+"""This module defines the SiteHost class.
+
+This file may be removed or left empty if no site customization is neccessary.
+base_classes.py contains logic to provision for this.
+
+Implementation details:
+You should import the "hosts" package instead of importing each type of host.
+
+ SiteHost: Host containing site-specific customizations.
+"""
+
+__author__ = """mbligh@google.com (Martin J. Bligh),
+poirier@google.com (Benjamin Poirier),
+stutsman@google.com (Ryan Stutsman)"""
+
+
+import base_classes
+
+
+class SiteHost(base_classes.Host):
+ """Custom host to containing site-specific methods or attributes.
+ """
+
+ def __init__(self):
+ super(SiteHost, self).__init__()
+
+ #def get_platform(self):
+ #...
+
+ #def get_bios_version(self):
+ #...
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