Roll tools/swarming_client/ to bdad1183d6047cc6a1459a5be167ba0a7325146e.
[chromium-blink-merge.git] / testing / legion / client_lib.py
blob4d6a633f0433ce22221652e5dd65338a80275f3f
1 # Copyright 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Defines the client library."""
7 import argparse
8 import datetime
9 import logging
10 import os
11 import socket
12 import subprocess
13 import sys
14 import tempfile
15 import threading
16 import xmlrpclib
18 #pylint: disable=relative-import
19 import common_lib
21 THIS_DIR = os.path.dirname(os.path.abspath(__file__))
22 SWARMING_DIR = os.path.join(THIS_DIR, '..', '..', 'tools/swarming_client')
23 ISOLATE_PY = os.path.join(SWARMING_DIR, 'isolate.py')
24 SWARMING_PY = os.path.join(SWARMING_DIR, 'swarming.py')
27 class Error(Exception):
28 pass
31 class ConnectionTimeoutError(Error):
32 pass
35 class ClientController(object):
36 """Creates, configures, and controls a client machine."""
38 _client_count = 0
39 _controllers = []
41 def __init__(self, isolate_file, config_vars, dimensions, priority=100,
42 idle_timeout_secs=common_lib.DEFAULT_TIMEOUT_SECS,
43 connection_timeout_secs=common_lib.DEFAULT_TIMEOUT_SECS,
44 verbosity='ERROR', name=None):
45 assert isinstance(config_vars, dict)
46 assert isinstance(dimensions, dict)
47 type(self)._controllers.append(self)
48 type(self)._client_count += 1
49 self.verbosity = verbosity
50 self._name = name or 'Client%d' % type(self)._client_count
51 self._priority = priority
52 self._isolate_file = isolate_file
53 self._isolated_file = isolate_file + 'd'
54 self._idle_timeout_secs = idle_timeout_secs
55 self._config_vars = config_vars
56 self._dimensions = dimensions
57 self._connect_event = threading.Event()
58 self._connected = False
59 self._ip_address = None
60 self._otp = self._CreateOTP()
61 self._rpc = None
63 parser = argparse.ArgumentParser()
64 parser.add_argument('--isolate-server')
65 parser.add_argument('--swarming-server')
66 parser.add_argument('--client-connection-timeout-secs',
67 default=common_lib.DEFAULT_TIMEOUT_SECS)
68 args, _ = parser.parse_known_args()
70 self._isolate_server = args.isolate_server
71 self._swarming_server = args.swarming_server
72 self._connection_timeout_secs = (connection_timeout_secs or
73 args.client_connection_timeout_secs)
75 @property
76 def name(self):
77 return self._name
79 @property
80 def otp(self):
81 return self._otp
83 @property
84 def connected(self):
85 return self._connected
87 @property
88 def connect_event(self):
89 return self._connect_event
91 @property
92 def rpc(self):
93 return self._rpc
95 @property
96 def verbosity(self):
97 return self._verbosity
99 @verbosity.setter
100 def verbosity(self, level):
101 """Sets the verbosity level as a string.
103 Either a string ('INFO', 'DEBUG', etc) or a logging level (logging.INFO,
104 logging.DEBUG, etc) is allowed.
106 assert isinstance(level, (str, int))
107 if isinstance(level, int):
108 level = logging.getLevelName(level)
109 self._verbosity = level #pylint: disable=attribute-defined-outside-init
111 @classmethod
112 def ReleaseAllControllers(cls):
113 for controller in cls._controllers:
114 controller.Release()
116 def _CreateOTP(self):
117 """Creates the OTP."""
118 host_name = socket.gethostname()
119 test_name = os.path.basename(sys.argv[0])
120 creation_time = datetime.datetime.utcnow()
121 otp = 'client:%s-host:%s-test:%s-creation:%s' % (
122 self._name, host_name, test_name, creation_time)
123 return otp
125 def Create(self):
126 """Creates the client machine."""
127 logging.info('Creating %s', self.name)
128 self._connect_event.clear()
129 self._ExecuteIsolate()
130 self._ExecuteSwarming()
132 def WaitForConnection(self):
133 """Waits for the client machine to connect.
135 Raises:
136 ConnectionTimeoutError if the client doesn't connect in time.
138 logging.info('Waiting for %s to connect with a timeout of %d seconds',
139 self._name, self._connection_timeout_secs)
140 self._connect_event.wait(self._connection_timeout_secs)
141 if not self._connect_event.is_set():
142 raise ConnectionTimeoutError('%s failed to connect' % self.name)
144 def Release(self):
145 """Quits the client's RPC server so it can release the machine."""
146 if self._rpc is not None and self._connected:
147 logging.info('Releasing %s', self._name)
148 try:
149 self._rpc.Quit()
150 except (socket.error, xmlrpclib.Fault):
151 logging.error('Unable to connect to %s to call Quit', self.name)
152 self._rpc = None
153 self._connected = False
155 def _ExecuteIsolate(self):
156 """Executes isolate.py."""
157 cmd = [
158 'python',
159 ISOLATE_PY,
160 'archive',
161 '--isolate', self._isolate_file,
162 '--isolated', self._isolated_file,
165 if self._isolate_server:
166 cmd.extend(['--isolate-server', self._isolate_server])
167 for key, value in self._config_vars.iteritems():
168 cmd.extend(['--config-var', key, value])
170 self._ExecuteProcess(cmd)
172 def _ExecuteSwarming(self):
173 """Executes swarming.py."""
174 cmd = [
175 'python',
176 SWARMING_PY,
177 'trigger',
178 self._isolated_file,
179 '--priority', str(self._priority),
182 if self._isolate_server:
183 cmd.extend(['--isolate-server', self._isolate_server])
184 if self._swarming_server:
185 cmd.extend(['--swarming', self._swarming_server])
186 for key, value in self._dimensions.iteritems():
187 cmd.extend(['--dimension', key, value])
189 cmd.extend([
190 '--',
191 '--host', common_lib.MY_IP,
192 '--otp', self._otp,
193 '--verbosity', self._verbosity,
194 '--idle-timeout', str(self._idle_timeout_secs),
197 self._ExecuteProcess(cmd)
199 def _ExecuteProcess(self, cmd):
200 """Executes a process, waits for it to complete, and checks for success."""
201 logging.debug('Running %s', ' '.join(cmd))
202 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
203 _, stderr = p.communicate()
204 if p.returncode != 0:
205 stderr.seek(0)
206 raise Error(stderr)
208 def OnConnect(self, ip_address):
209 """Receives client ip address on connection."""
210 self._ip_address = ip_address
211 self._connected = True
212 self._rpc = common_lib.ConnectToServer(self._ip_address)
213 logging.info('%s connected from %s', self._name, ip_address)
214 self._connect_event.set()