Chromium Code Reviews| Index: testing/legion/client_lib.py |
| diff --git a/testing/legion/client_lib.py b/testing/legion/client_lib.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..2576e4f9d41ac8415f7425367762bab824c977b4 |
| --- /dev/null |
| +++ b/testing/legion/client_lib.py |
| @@ -0,0 +1,240 @@ |
| +# 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. |
| +"""Client library module.""" |
| + |
| +import argparse |
| +import logging |
| +import os |
| +import socket |
| +import subprocess |
| +import tempfile |
| +import threading |
| +import uuid |
| +import xmlrpclib |
| + |
| +#pylint: disable=relative-import |
| +import common_lib |
| +import client_rpc_server |
| + |
| +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') |
| +# ISOLATE_SERVER = 'omnibot-isolate-server.appspot.com' |
| +# SWARMING_SERVER = 'https://omnibot-swarming-server.appspot.com/' |
| +CLIENT_CONNECTION_TIMEOUT = 30 * 60 # 30 minutes |
| + |
| + |
| +class Error(Exception): |
| + pass |
| + |
| + |
| +class ConnectionTimeoutError(Error): |
| + pass |
| + |
| + |
| +class Client(object): |
| + """The main client class. |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
Few issues:
- The class name is misleading, it's n
Mike Meade
2015/02/03 01:18:08
Done.
|
| + |
| + This class is used to create clients, wait for them to connect to the host, |
| + connect to their RPC servers, and run RPC commands. |
| + """ |
| + |
| + _client_count = 0 |
| + |
| + def __init__(self, isolate_file, discovery_server, name=None): |
| + """ctor. |
| + |
| + Args: |
| + isolate_file: The path to the client isolate file. |
| + discovery_server: The discovery server to register with |
| + name: A name for the client. |
| + """ |
| + self._IncreaseCount() |
| + self.name = name or self._CreateName() |
| + self.priority = 100 |
| + self.isolate_file = isolate_file |
| + self.isolated_file = isolate_file + 'd' |
| + self.rpc_timeout = None |
| + self._connect_event = threading.Event() |
| + self._args = self._ParseArgs() |
| + self._discovery_server = discovery_server |
| + self._connected = False |
| + self._ip_address = None |
| + self._config_vars = [] |
| + self._dimensions = [] |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
this must be a dict, since duplicate keys are not
Mike Meade
2015/02/03 01:18:08
Done.
|
| + self._otp = str(uuid.uuid1()) |
| + self._rpc = None |
| + self._verbose = False |
| + |
| + @property |
| + def connected(self): |
| + """Return the value of self._connected.""" |
| + return self._connected |
| + |
| + @property |
| + def connect_event(self): |
| + return self._connect_event |
| + |
| + @property |
| + def rpc(self): |
| + return self._rpc |
| + |
| + @classmethod |
| + def _IncreaseCount(cls): |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:38
Why a function if it is used exactly at one place?
Mike Meade
2015/02/03 01:18:08
Changed it to type(self)._client_count += 1 in the
|
| + """Increase the client_count parameter.""" |
| + cls._client_count += 1 |
| + |
| + @classmethod |
| + def _CreateName(cls): |
| + """Create a name for this client. |
| + |
| + By default the name is "Client%d" where %d is the number of clients that |
| + currently exist. |
| + """ |
| + return 'Client%d' % cls._client_count |
| + |
| + def _ParseArgs(self): |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
Why is this a method?
Mike Meade
2015/02/03 01:18:08
Moved it into __init__
|
| + """Parse command line args.""" |
| + parser = argparse.ArgumentParser() |
| + parser.add_argument('--isolate-server') |
| + parser.add_argument('--swarming-server') |
| + parser.add_argument('--client-connection-timeout', |
| + default=CLIENT_CONNECTION_TIMEOUT) |
| + args, _ = parser.parse_known_args() |
| + return args |
| + |
| + def AddConfigVars(self, key, value): |
| + """Add a set of config vars to isolate.py. |
| + |
| + Args: |
| + key: The config vars key. |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
Frankly, I don't see the value of this documentati
Mike Meade
2015/02/03 01:18:08
Done.
|
| + value: The config vars value. |
| + """ |
| + logging.debug('Adding --config-var %s %s to %s', key, value, |
| + self.name) |
| + self._config_vars.append((key, value)) |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
The fact previous keys are not overridden IS somet
Mike Meade
2015/02/03 01:18:07
I added a note, but should this also be a dict?
|
| + |
| + def AddDimension(self, key, value): |
| + """Add a set of dimensions to swarming.py. |
| + |
| + Args: |
| + key: The dimension key. |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
same
Mike Meade
2015/02/03 01:18:08
Done.
|
| + value: The dimension value. |
| + """ |
| + logging.debug('Adding --dimension %s %s to %s', key, value, |
| + self.name) |
| + self._dimensions.append((key, value)) |
| + |
| + def Create(self, wait=False, timeout=None): |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:38
Why options? Will there be code that will use all
Mike Meade
2015/02/03 01:18:07
In thinking about it more this is probably over-en
|
| + """Create the client machine and wait for it to be created if specified. |
| + |
| + Args: |
| + wait: True to block until created, False to return immediately. |
| + timeout: The timeout to block before raising a ConnectionTimeoutError. |
| + """ |
| + logging.info('Creating %s', self.name) |
| + self._connect_event.clear() |
| + self._RegisterOnConnectCallback() |
| + self._ExecuteIsolate() |
| + self._ExecuteSwarming() |
| + if wait: |
| + self.WaitForConnection(timeout) |
| + |
| + def WaitForConnection(self, timeout=None): |
| + """Connect to the client machine. |
| + |
| + This method waits for the client machine to register itself |
| + with the discovery server. |
| + |
| + Args: |
| + timeout: The timeout in seconds. |
| + |
| + Raises: |
| + TimeoutError if the client doesn't connect in time. |
| + """ |
| + timeout = timeout or self._args.client_connection_timeout |
| + msg = ('Waiting for %s to connect with a timeout of %d seconds' % |
| + (self.name, timeout)) |
| + logging.info(msg) |
| + self._connect_event.wait(timeout) |
| + if not self._connect_event.is_set(): |
| + raise ConnectionTimeoutError() |
| + |
| + def Release(self): |
| + """Quit 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): |
| + """Execute isolate.py with the given args.""" |
| + cmd = [ |
| + 'python', |
| + ISOLATE_PY, |
| + 'archive', |
| + '--isolate=' + self.isolate_file, |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
Trust me, that's a recipe for bugs. Use instead:
'
Mike Meade
2015/02/03 01:18:07
Done.
|
| + '--isolated=' + self.isolated_file, |
| + ] |
| + |
| + if self._args.isolate_server: |
| + cmd += ['--isolate-server', self._args.isolate_server] |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
I generally prefer cmd.extend(...)
Mike Meade
2015/02/03 01:18:07
Done.
|
| + for key, value in self._config_vars: |
| + cmd += ['--config-var', key, value] |
| + |
| + logging.debug('Running %s', ' '.join(cmd)) |
| + if subprocess.call(cmd, stdout=subprocess.PIPE) != 0: |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
This may hang BTW since you are not reading stdout
Mike Meade
2015/02/03 01:18:08
Done.
|
| + raise Error('Error calling isolate.py') |
| + |
| + def _ExecuteSwarming(self): |
| + """Execute swarming.py with the vars.""" |
| + cmd = [ |
| + 'python', |
| + SWARMING_PY, |
| + 'trigger', |
| + self.isolated_file, |
| + '--priority=' + str(self.priority) |
| + ] |
| + |
| + if self._args.isolate_server: |
| + cmd += ['--isolate-server', self._args.isolate_server] |
| + if self._args.swarming_server: |
| + cmd += ['--swarming', self._args.swarming_server] |
| + for key, value in self._dimensions: |
| + cmd += ['--dimension', key, value] |
| + |
| + cmd += ['--', '--host', str(common_lib.MY_IP), '--otp', self._otp] |
| + if self.rpc_timeout: |
| + cmd += ['--idle-timeout', str(self.rpc_timeout)] |
| + if self._verbose: |
| + cmd += ['--verbose'] |
| + |
| + logging.debug('Running %s', ' '.join(cmd)) |
| + if subprocess.call(cmd, stdout=subprocess.PIPE) != 0: |
| + raise Error('Error calling swarming.py') |
| + |
| + def _RegisterOnConnectCallback(self): |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
Same thing, it's used exactly at one place, I don'
Mike Meade
2015/02/03 01:18:08
Done.
|
| + """Register a callback with the discovery server. |
| + |
| + This callback is used to receive the client's IP address once it starts |
|
Marc-Antoine Ruel (Google)
2015/01/30 21:58:39
What the callback does is not relevant documentati
Mike Meade
2015/02/03 01:18:07
Acknowledged.
|
| + and contacts the discovery server. |
| + """ |
| + self._discovery_server.RegisterClientCallback(self._otp, self._OnConnect) |
| + |
| + def _OnConnect(self, ip_address): |
| + """The OnConnect callback method. |
| + |
| + This method receives the ip address received by the discovery server from |
| + the client and sets the object's connected state to True. |
| + """ |
| + self._ip_address = ip_address |
| + self._connected = True |
| + self._rpc = client_rpc_server.RPCServer.Connect(self._ip_address) |
| + logging.info('%s connected from %s', self.name, ip_address) |
| + self._connect_event.set() |