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