summaryrefslogtreecommitdiffstats
path: root/testing/legion
diff options
context:
space:
mode:
authormmeade <mmeade@chromium.org>2015-02-06 11:03:15 -0800
committerCommit bot <commit-bot@chromium.org>2015-02-06 19:03:58 +0000
commitabebda49c4269e96a48fe435272b3ddb6cccb942 (patch)
tree4d3e6bc1348169e47ba40a7db5bb81b520f1b81c /testing/legion
parent809e023343ae13d40f21e4d03ff7b09b5ecf005a (diff)
downloadchromium_src-abebda49c4269e96a48fe435272b3ddb6cccb942.zip
chromium_src-abebda49c4269e96a48fe435272b3ddb6cccb942.tar.gz
chromium_src-abebda49c4269e96a48fe435272b3ddb6cccb942.tar.bz2
Adding the initial code for Omnibot multi-machine support.
This cl contains the base code plus a "simple" hello world example test. BUG=453679 Review URL: https://codereview.chromium.org/890773003 Cr-Commit-Position: refs/heads/master@{#315080}
Diffstat (limited to 'testing/legion')
-rw-r--r--testing/legion/__init__.py3
-rwxr-xr-xtesting/legion/client_controller.py51
-rw-r--r--testing/legion/client_lib.py214
-rw-r--r--testing/legion/client_rpc_methods.py42
-rw-r--r--testing/legion/client_rpc_server.py128
-rw-r--r--testing/legion/common_lib.py40
-rw-r--r--testing/legion/discovery_server.py55
-rw-r--r--testing/legion/examples/hello_world/client_test.isolate23
-rwxr-xr-xtesting/legion/examples/hello_world/client_test.py27
-rw-r--r--testing/legion/examples/hello_world/host_test.isolate22
-rwxr-xr-xtesting/legion/examples/hello_world/host_test.py74
-rw-r--r--testing/legion/host_controller.py70
-rw-r--r--testing/legion/legion.isolate20
13 files changed, 769 insertions, 0 deletions
diff --git a/testing/legion/__init__.py b/testing/legion/__init__.py
new file mode 100644
index 0000000..50b23df
--- /dev/null
+++ b/testing/legion/__init__.py
@@ -0,0 +1,3 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
diff --git a/testing/legion/client_controller.py b/testing/legion/client_controller.py
new file mode 100755
index 0000000..dd80c29
--- /dev/null
+++ b/testing/legion/client_controller.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""The main client_controller code.
+
+This code is the main entry point for the client machines and handles
+registering with the host server and running the local RPC server.
+"""
+
+import argparse
+import logging
+import socket
+import sys
+import time
+
+#pylint: disable=relative-import
+import client_rpc_server
+import common_lib
+
+
+def main():
+ print ' '.join(sys.argv)
+ common_lib.InitLogging()
+ logging.info('Client controller starting')
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--otp',
+ help='One time token used to authenticate with the host')
+ parser.add_argument('--host',
+ help='The ip address of the host')
+ parser.add_argument('--idle-timeout', type=int,
+ default=common_lib.DEFAULT_TIMEOUT_SECS,
+ help='The idle timeout for the rpc server in seconds')
+ args, _ = parser.parse_known_args()
+
+ logging.info(
+ 'Registering with discovery server at %s using OTP %s', args.host,
+ args.otp)
+ server = common_lib.ConnectToServer(args.host).RegisterClient(
+ args.otp, common_lib.MY_IP)
+
+ server = client_rpc_server.RPCServer(args.host, args.idle_timeout)
+
+ server.serve_forever()
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/testing/legion/client_lib.py b/testing/legion/client_lib.py
new file mode 100644
index 0000000..4d6a633
--- /dev/null
+++ b/testing/legion/client_lib.py
@@ -0,0 +1,214 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Defines the client library."""
+
+import argparse
+import datetime
+import logging
+import os
+import socket
+import subprocess
+import sys
+import tempfile
+import threading
+import xmlrpclib
+
+#pylint: disable=relative-import
+import common_lib
+
+THIS_DIR = os.path.dirname(os.path.abspath(__file__))
+SWARMING_DIR = os.path.join(THIS_DIR, '..', '..', 'tools/swarming_client')
+ISOLATE_PY = os.path.join(SWARMING_DIR, 'isolate.py')
+SWARMING_PY = os.path.join(SWARMING_DIR, 'swarming.py')
+
+
+class Error(Exception):
+ pass
+
+
+class ConnectionTimeoutError(Error):
+ pass
+
+
+class ClientController(object):
+ """Creates, configures, and controls a client machine."""
+
+ _client_count = 0
+ _controllers = []
+
+ def __init__(self, isolate_file, config_vars, dimensions, priority=100,
+ idle_timeout_secs=common_lib.DEFAULT_TIMEOUT_SECS,
+ connection_timeout_secs=common_lib.DEFAULT_TIMEOUT_SECS,
+ verbosity='ERROR', name=None):
+ assert isinstance(config_vars, dict)
+ assert isinstance(dimensions, dict)
+ type(self)._controllers.append(self)
+ type(self)._client_count += 1
+ self.verbosity = verbosity
+ self._name = name or 'Client%d' % type(self)._client_count
+ self._priority = priority
+ self._isolate_file = isolate_file
+ self._isolated_file = isolate_file + 'd'
+ self._idle_timeout_secs = idle_timeout_secs
+ self._config_vars = config_vars
+ self._dimensions = dimensions
+ self._connect_event = threading.Event()
+ self._connected = False
+ self._ip_address = None
+ self._otp = self._CreateOTP()
+ self._rpc = None
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--isolate-server')
+ parser.add_argument('--swarming-server')
+ parser.add_argument('--client-connection-timeout-secs',
+ default=common_lib.DEFAULT_TIMEOUT_SECS)
+ args, _ = parser.parse_known_args()
+
+ self._isolate_server = args.isolate_server
+ self._swarming_server = args.swarming_server
+ self._connection_timeout_secs = (connection_timeout_secs or
+ args.client_connection_timeout_secs)
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def otp(self):
+ return self._otp
+
+ @property
+ def connected(self):
+ return self._connected
+
+ @property
+ def connect_event(self):
+ return self._connect_event
+
+ @property
+ def rpc(self):
+ return self._rpc
+
+ @property
+ def verbosity(self):
+ return self._verbosity
+
+ @verbosity.setter
+ def verbosity(self, level):
+ """Sets the verbosity level as a string.
+
+ Either a string ('INFO', 'DEBUG', etc) or a logging level (logging.INFO,
+ logging.DEBUG, etc) is allowed.
+ """
+ assert isinstance(level, (str, int))
+ if isinstance(level, int):
+ level = logging.getLevelName(level)
+ self._verbosity = level #pylint: disable=attribute-defined-outside-init
+
+ @classmethod
+ def ReleaseAllControllers(cls):
+ for controller in cls._controllers:
+ controller.Release()
+
+ def _CreateOTP(self):
+ """Creates the OTP."""
+ host_name = socket.gethostname()
+ test_name = os.path.basename(sys.argv[0])
+ creation_time = datetime.datetime.utcnow()
+ otp = 'client:%s-host:%s-test:%s-creation:%s' % (
+ self._name, host_name, test_name, creation_time)
+ return otp
+
+ def Create(self):
+ """Creates the client machine."""
+ logging.info('Creating %s', self.name)
+ self._connect_event.clear()
+ self._ExecuteIsolate()
+ self._ExecuteSwarming()
+
+ def WaitForConnection(self):
+ """Waits for the client machine to connect.
+
+ Raises:
+ ConnectionTimeoutError if the client doesn't connect in time.
+ """
+ logging.info('Waiting for %s to connect with a timeout of %d seconds',
+ self._name, self._connection_timeout_secs)
+ self._connect_event.wait(self._connection_timeout_secs)
+ if not self._connect_event.is_set():
+ raise ConnectionTimeoutError('%s failed to connect' % self.name)
+
+ def Release(self):
+ """Quits the client's RPC server so it can release the machine."""
+ if self._rpc is not None and self._connected:
+ logging.info('Releasing %s', self._name)
+ try:
+ self._rpc.Quit()
+ except (socket.error, xmlrpclib.Fault):
+ logging.error('Unable to connect to %s to call Quit', self.name)
+ self._rpc = None
+ self._connected = False
+
+ def _ExecuteIsolate(self):
+ """Executes isolate.py."""
+ cmd = [
+ 'python',
+ ISOLATE_PY,
+ 'archive',
+ '--isolate', self._isolate_file,
+ '--isolated', self._isolated_file,
+ ]
+
+ if self._isolate_server:
+ cmd.extend(['--isolate-server', self._isolate_server])
+ for key, value in self._config_vars.iteritems():
+ cmd.extend(['--config-var', key, value])
+
+ self._ExecuteProcess(cmd)
+
+ def _ExecuteSwarming(self):
+ """Executes swarming.py."""
+ cmd = [
+ 'python',
+ SWARMING_PY,
+ 'trigger',
+ self._isolated_file,
+ '--priority', str(self._priority),
+ ]
+
+ if self._isolate_server:
+ cmd.extend(['--isolate-server', self._isolate_server])
+ if self._swarming_server:
+ cmd.extend(['--swarming', self._swarming_server])
+ for key, value in self._dimensions.iteritems():
+ cmd.extend(['--dimension', key, value])
+
+ cmd.extend([
+ '--',
+ '--host', common_lib.MY_IP,
+ '--otp', self._otp,
+ '--verbosity', self._verbosity,
+ '--idle-timeout', str(self._idle_timeout_secs),
+ ])
+
+ self._ExecuteProcess(cmd)
+
+ def _ExecuteProcess(self, cmd):
+ """Executes a process, waits for it to complete, and checks for success."""
+ logging.debug('Running %s', ' '.join(cmd))
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ _, stderr = p.communicate()
+ if p.returncode != 0:
+ stderr.seek(0)
+ raise Error(stderr)
+
+ def OnConnect(self, ip_address):
+ """Receives client ip address on connection."""
+ self._ip_address = ip_address
+ self._connected = True
+ self._rpc = common_lib.ConnectToServer(self._ip_address)
+ logging.info('%s connected from %s', self._name, ip_address)
+ self._connect_event.set()
diff --git a/testing/legion/client_rpc_methods.py b/testing/legion/client_rpc_methods.py
new file mode 100644
index 0000000..24a552e
--- /dev/null
+++ b/testing/legion/client_rpc_methods.py
@@ -0,0 +1,42 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Defines the client RPC methods."""
+
+import logging
+import subprocess
+import threading
+
+
+class RPCMethods(object):
+ """Class exposing RPC methods."""
+
+ def __init__(self, server):
+ self.server = server
+
+ def Echo(self, message):
+ """Simple RPC method to print and return a message."""
+ logging.info('Echoing %s', message)
+ return 'echo %s' % str(message)
+
+ def Subprocess(self, cmd):
+ """Run the commands in a subprocess.
+
+ Returns:
+ (returncode, stdout, stderr).
+ """
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ return (p.returncode, stdout, stderr)
+
+ def Quit(self):
+ """Call server.shutdown in another thread.
+
+ This is needed because server.shutdown waits for the server to actually
+ quit. However the server cannot shutdown until it completes handling this
+ call. Calling this in the same thread results in a deadlock.
+ """
+ t = threading.Thread(target=self.server.shutdown)
+ t.start()
diff --git a/testing/legion/client_rpc_server.py b/testing/legion/client_rpc_server.py
new file mode 100644
index 0000000..7a5f565
--- /dev/null
+++ b/testing/legion/client_rpc_server.py
@@ -0,0 +1,128 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""The client RPC server code.
+
+This server is an XML-RPC server which serves code from
+client_rpc_methods.RPCMethods.
+
+This server will run until shutdown is called on the server object. This can
+be achieved in 2 ways:
+
+- Calling the Quit RPC method defined in RPCMethods
+- Not receiving any calls within the idle_timeout_secs time.
+"""
+
+import logging
+import threading
+import time
+import xmlrpclib
+import SimpleXMLRPCServer
+import SocketServer
+
+#pylint: disable=relative-import
+import client_rpc_methods
+import common_lib
+
+
+class RequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
+ """Restricts access to only specified IP address.
+
+ This call assumes the server is RPCServer.
+ """
+
+ def do_POST(self):
+ """Verifies the client is authorized to perform RPCs."""
+ if self.client_address[0] != self.server.authorized_address:
+ logging.error('Received unauthorized RPC request from %s',
+ self.client_address[0])
+ self.send_response(403)
+ response = 'Forbidden'
+ self.send_header('Content-type', 'text/plain')
+ self.send_header('Content-length', str(len(response)))
+ self.end_headers()
+ self.wfile.write(response)
+ else:
+ return SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.do_POST(self)
+
+
+class RPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer,
+ SocketServer.ThreadingMixIn):
+ """Restricts all endpoints to only specified IP addresses."""
+
+ def __init__(self, authorized_address,
+ idle_timeout_secs=common_lib.DEFAULT_TIMEOUT_SECS):
+ SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(
+ self, (common_lib.SERVER_ADDRESS, common_lib.SERVER_PORT),
+ allow_none=True, logRequests=False,
+ requestHandler=RequestHandler)
+
+ self.authorized_address = authorized_address
+ self.idle_timeout_secs = idle_timeout_secs
+ self.register_instance(client_rpc_methods.RPCMethods(self))
+
+ self._shutdown_requested_event = threading.Event()
+ self._rpc_received_event = threading.Event()
+ self._idle_thread = threading.Thread(target=self._CheckForIdleQuit)
+
+ def shutdown(self):
+ """Shutdown the server.
+
+ This overloaded method sets the _shutdown_requested_event to allow the
+ idle timeout thread to quit.
+ """
+ self._shutdown_requested_event.set()
+ SimpleXMLRPCServer.SimpleXMLRPCServer.shutdown(self)
+ logging.info('Server shutdown complete')
+
+ def serve_forever(self, poll_interval=0.5):
+ """Serve forever.
+
+ This overloaded method starts the idle timeout thread before calling
+ serve_forever. This ensures the idle timer thread doesn't get started
+ without the server running.
+
+ Args:
+ poll_interval: The interval to poll for shutdown.
+ """
+ logging.info('RPC server starting')
+ self._idle_thread.start()
+ SimpleXMLRPCServer.SimpleXMLRPCServer.serve_forever(self, poll_interval)
+
+ def _dispatch(self, method, params):
+ """Dispatch the call to the correct method with the provided params.
+
+ This overloaded method adds logging to help trace connection and
+ call problems.
+
+ Args:
+ method: The method name to call.
+ params: A tuple of parameters to pass.
+
+ Returns:
+ The result of the parent class' _dispatch method.
+ """
+ logging.debug('Calling %s%s', method, params)
+ self._rpc_received_event.set()
+ return SimpleXMLRPCServer.SimpleXMLRPCServer._dispatch(self, method, params)
+
+ def _CheckForIdleQuit(self):
+ """Check for, and exit, if the server is idle for too long.
+
+ This method must be run in a separate thread to avoid a deadlock when
+ calling server.shutdown.
+ """
+ timeout = time.time() + self.idle_timeout_secs
+ while time.time() < timeout:
+ if self._shutdown_requested_event.is_set():
+ # An external source called shutdown()
+ return
+ elif self._rpc_received_event.is_set():
+ logging.debug('Resetting the idle timeout')
+ timeout = time.time() + self.idle_timeout_secs
+ self._rpc_received_event.clear()
+ time.sleep(1)
+ # We timed out, kill the server
+ logging.warning('Shutting down the server due to the idle timeout')
+ self.shutdown()
diff --git a/testing/legion/common_lib.py b/testing/legion/common_lib.py
new file mode 100644
index 0000000..b66481e
--- /dev/null
+++ b/testing/legion/common_lib.py
@@ -0,0 +1,40 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Common library methods used by both host and client controllers."""
+
+import argparse
+import logging
+import socket
+import xmlrpclib
+
+LOGGING_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'WARN', 'ERROR']
+MY_IP = socket.gethostbyname(socket.gethostname())
+SERVER_ADDRESS = ''
+SERVER_PORT = 31710
+DEFAULT_TIMEOUT_SECS = 20 * 60 # 30 minutes
+
+
+def InitLogging():
+ """Initialize the logging module.
+
+ Raises:
+ argparse.ArgumentError if the --verbosity arg is incorrect.
+ """
+ parser = argparse.ArgumentParser()
+ logging_action = parser.add_argument('--verbosity', default='ERROR')
+ args, _ = parser.parse_known_args()
+ if args.verbosity not in LOGGING_LEVELS:
+ raise argparse.ArgumentError(
+ logging_action, 'Only levels %s supported' % str(LOGGING_LEVELS))
+ logging.basicConfig(
+ format='%(asctime)s %(filename)s:%(lineno)s %(levelname)s] %(message)s',
+ datefmt='%H:%M:%S', level=args.verbosity)
+
+
+def ConnectToServer(server):
+ """Connect to an RPC server."""
+ addr = 'http://%s:%d' % (server, SERVER_PORT)
+ logging.debug('Connecting to RPC server at %s', addr)
+ return xmlrpclib.Server(addr)
diff --git a/testing/legion/discovery_server.py b/testing/legion/discovery_server.py
new file mode 100644
index 0000000..94786ce
--- /dev/null
+++ b/testing/legion/discovery_server.py
@@ -0,0 +1,55 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""The discovery server used to register clients.
+
+The discovery server is started by the host controller and allows the clients
+to register themselves when they start. Authentication of the client controllers
+is based on an OTP passed to the client controller binary on startup.
+"""
+
+import logging
+import threading
+import xmlrpclib
+import SimpleXMLRPCServer
+
+#pylint: disable=relative-import
+import common_lib
+
+
+class DiscoveryServer(object):
+ """Discovery server run on the host."""
+
+ def __init__(self):
+ self._expected_clients = {}
+ self._rpc_server = None
+ self._thread = None
+
+ def _RegisterClientRPC(self, otp, ip):
+ """The RPC used by a client to register with the discovery server."""
+ assert otp in self._expected_clients
+ cb = self._expected_clients.pop(otp)
+ cb(ip)
+
+ def RegisterClientCallback(self, otp, callback):
+ """Registers a callback associated with an OTP."""
+ assert callable(callback)
+ self._expected_clients[otp] = callback
+
+ def Start(self):
+ """Starts the discovery server."""
+ logging.debug('Starting discovery server')
+ self._rpc_server = SimpleXMLRPCServer.SimpleXMLRPCServer(
+ (common_lib.SERVER_ADDRESS, common_lib.SERVER_PORT),
+ allow_none=True, logRequests=False)
+ self._rpc_server.register_function(
+ self._RegisterClientRPC, 'RegisterClient')
+ self._thread = threading.Thread(target=self._rpc_server.serve_forever)
+ self._thread.start()
+
+ def Shutdown(self):
+ """Shuts the discovery server down."""
+ if self._thread and self._thread.is_alive():
+ logging.debug('Shutting down discovery server')
+ self._rpc_server.shutdown()
diff --git a/testing/legion/examples/hello_world/client_test.isolate b/testing/legion/examples/hello_world/client_test.isolate
new file mode 100644
index 0000000..7135ef2
--- /dev/null
+++ b/testing/legion/examples/hello_world/client_test.isolate
@@ -0,0 +1,23 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+{
+ 'includes': [
+ '../../legion.isolate'
+ ],
+ 'conditions': [
+ ['multi_machine == 1', {
+ 'variables': {
+ 'command': [
+ 'python',
+ '../../client_controller.py',
+ ],
+ 'files': [
+ 'client_test.py',
+ 'client_test.isolate'
+ ],
+ },
+ }],
+ ],
+}
diff --git a/testing/legion/examples/hello_world/client_test.py b/testing/legion/examples/hello_world/client_test.py
new file mode 100755
index 0000000..0333f7b
--- /dev/null
+++ b/testing/legion/examples/hello_world/client_test.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""A simple client test module.
+
+This module is invoked by the host by calling the client controller's
+Subprocess RPC method. The name is passed in as a required argument on the
+command line.
+"""
+
+import argparse
+import os
+import sys
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('name')
+ args = parser.parse_args()
+ print 'Hello world from', args.name
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/testing/legion/examples/hello_world/host_test.isolate b/testing/legion/examples/hello_world/host_test.isolate
new file mode 100644
index 0000000..da4ee4e
--- /dev/null
+++ b/testing/legion/examples/hello_world/host_test.isolate
@@ -0,0 +1,22 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+{
+ 'includes': [
+ '../../legion.isolate',
+ 'client_test.isolate'
+ ],
+ 'conditions': [
+ ['multi_machine == 1', {
+ 'variables': {
+ 'command': [
+ 'host_test.py',
+ ],
+ 'files': [
+ 'host_test.py',
+ ],
+ },
+ }],
+ ]
+}
diff --git a/testing/legion/examples/hello_world/host_test.py b/testing/legion/examples/hello_world/host_test.py
new file mode 100755
index 0000000..77eca06
--- /dev/null
+++ b/testing/legion/examples/hello_world/host_test.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""A simple host test module.
+
+This module runs on the host machine and is responsible for creating 2
+client machines, waiting for them, and running RPC calls on them.
+"""
+
+# Map the legion directory so we can import the host controller.
+import sys
+sys.path.append('../../')
+
+import logging
+import time
+
+import host_controller
+
+
+class ExampleController(host_controller.HostController):
+ """A simple example controller for a test."""
+
+ def __init__(self):
+ super(ExampleController, self).__init__()
+ self.client1 = None
+ self.client2 = None
+
+ def CreateClient(self):
+ """Create a client object and set the proper values."""
+ client = self.NewClient(
+ isolate_file='client_test.isolate',
+ config_vars={'multi_machine': '1'},
+ dimensions={'os': 'Linux', 'pool': 'legion'}, priority=200,
+ idle_timeout_secs=90, connection_timeout_secs=90,
+ verbosity=logging.INFO)
+ client.Create()
+ return client
+
+ def SetUp(self):
+ """Create the client machines and wait until they connect.
+
+ In this call the actual creation of the client machines is done in parallel
+ by the system. The WaitForConnect calls are performed in series but will
+ return as soon as the clients connect.
+ """
+ self.client1 = self.CreateClient()
+ self.client2 = self.CreateClient()
+ self.client1.WaitForConnection()
+ self.client2.WaitForConnection()
+
+ def Task(self):
+ """Main method to run the task code."""
+ self.CallEcho(self.client1)
+ self.CallEcho(self.client2)
+ self.CallClientTest(self.client1)
+ self.CallClientTest(self.client2)
+
+ def CallEcho(self, client):
+ """Call rpc.Echo on a client."""
+ logging.info('Calling Echo on %s', client.name)
+ logging.info(self.client1.rpc.Echo(client.name))
+
+ def CallClientTest(self, client):
+ """Call client_test.py name on a client."""
+ logging.info('Calling Subprocess to run "./client_test.py %s"', client.name)
+ retcode, stdout, stderr = client.rpc.Subprocess(
+ ['./client_test.py', client.name])
+ logging.info('retcode: %s, stdout: %s, stderr: %s', retcode, stdout, stderr)
+
+
+if __name__ == '__main__':
+ ExampleController().RunController()
diff --git a/testing/legion/host_controller.py b/testing/legion/host_controller.py
new file mode 100644
index 0000000..dadcba4
--- /dev/null
+++ b/testing/legion/host_controller.py
@@ -0,0 +1,70 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Defines the host controller base library.
+
+This module is the basis on which host controllers are built and executed.
+"""
+
+import logging
+import sys
+
+#pylint: disable=relative-import
+import client_lib
+import common_lib
+import discovery_server
+
+
+class HostController(object):
+ """The base host controller class."""
+
+ def __init__(self):
+ self._discovery_server = discovery_server.DiscoveryServer()
+
+ def SetUp(self):
+ """Setup method used by the subclass."""
+ pass
+
+ def Task(self):
+ """Main task method used by the subclass."""
+ pass
+
+ def TearDown(self):
+ """Teardown method used by the subclass."""
+ pass
+
+ def NewClient(self, *args, **kwargs):
+ controller = client_lib.ClientController(*args, **kwargs)
+ self._discovery_server.RegisterClientCallback(
+ controller.otp, controller.OnConnect)
+ return controller
+
+ def RunController(self):
+ """Main entry point for the controller."""
+ print ' '.join(sys.argv)
+ common_lib.InitLogging()
+ self._discovery_server.Start()
+
+ error = None
+ tb = None
+ try:
+ self.SetUp()
+ self.Task()
+ except Exception as e:
+ # Defer raising exceptions until after TearDown and _TearDown are called.
+ error = e
+ tb = sys.exc_info()[-1]
+ try:
+ self.TearDown()
+ except Exception as e:
+ # Defer raising exceptions until after _TearDown is called.
+ # Note that an error raised here will obscure any errors raised
+ # previously.
+ error = e
+ tb = sys.exc_info()[-1]
+
+ self._discovery_server.Shutdown()
+ client_lib.ClientController.ReleaseAllControllers()
+ if error:
+ raise error, None, tb #pylint: disable=raising-bad-type
diff --git a/testing/legion/legion.isolate b/testing/legion/legion.isolate
new file mode 100644
index 0000000..463764d
--- /dev/null
+++ b/testing/legion/legion.isolate
@@ -0,0 +1,20 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+{
+ 'variables': {
+ 'files': [
+ '__init__.py',
+ 'client_controller.py',
+ 'client_lib.py',
+ 'client_rpc_methods.py',
+ 'client_rpc_server.py',
+ 'common_lib.py',
+ 'discovery_server.py',
+ 'host_controller.py',
+ 'legion.isolate',
+ '../../tools/swarming_client/',
+ ],
+ },
+}