OLD | NEW |
---|---|
(Empty) | |
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. | |
4 import argparse | |
Marc-Antoine Ruel (Google)
2015/01/30 18:00:21
In general we keep an empty line in between
Mike Meade
2015/01/30 21:04:04
Done.
| |
5 import logging | |
6 import os | |
7 import socket | |
8 import subprocess | |
9 import tempfile | |
10 import threading | |
11 import uuid | |
12 import xmlrpclib | |
13 | |
14 #pylint: disable=relative-import | |
15 import common_lib | |
16 import client_rpc_server | |
17 | |
18 THIS_DIR = os.path.dirname(os.path.abspath(__file__)) | |
19 SWARMING_DIR = os.path.join(THIS_DIR, '../../tools/swarming_client') | |
Marc-Antoine Ruel (Google)
2015/01/30 18:00:21
if you plan this to work on windows, use os.path.j
Mike Meade
2015/01/30 21:04:04
Done.
| |
20 ISOLATE_PY = os.path.join(SWARMING_DIR, 'isolate.py') | |
21 SWARMING_PY = os.path.join(SWARMING_DIR, 'swarming.py') | |
22 # ISOLATE_SERVER = 'omnibot-isolate-server.appspot.com' | |
23 # SWARMING_SERVER = 'https://omnibot-swarming-server.appspot.com/' | |
24 CLIENT_CONNECTION_TIMEOUT = 30 * 60 # 30 minutes | |
25 args = None | |
26 | |
27 | |
28 class Error(Exception): | |
29 pass | |
30 | |
31 | |
32 class ConnectionTimeoutError(Error): | |
33 pass | |
34 | |
35 | |
36 def GetArgs(): | |
37 """Parse command line args args. | |
38 | |
39 Returns: | |
40 Parsed command line args. | |
41 """ | |
42 global args | |
Marc-Antoine Ruel (Google)
2015/01/30 18:00:21
not a fan
Mike Meade
2015/01/30 21:04:04
Moved into the class.
| |
43 if not args: | |
44 parser = argparse.ArgumentParser() | |
45 parser.add_argument('--isolate-server') | |
46 parser.add_argument('--swarming-server') | |
47 parser.add_argument('--client-connection-timeout', | |
48 default=CLIENT_CONNECTION_TIMEOUT) | |
49 args, _ = parser.parse_known_args() | |
50 return args | |
51 | |
52 | |
53 class Client(object): | |
54 """The main client class. | |
55 | |
56 This class is used to create clients, connect to their RPC servers, and | |
57 run RPC commands. | |
58 """ | |
59 | |
60 _client_count = 0 | |
61 | |
62 def __init__(self, isolate_file, discovery_server, name=None): | |
63 """ctor. | |
64 | |
65 Args: | |
66 isolate_file: The path to the client isolate file. | |
67 discovery_server: The discovery server to register with | |
68 name: A name for the client. | |
69 """ | |
70 self._IncreaseCount() | |
71 self._discovery_server = discovery_server | |
72 self._name = name or self._CreateName() | |
73 self._priority = 100 | |
74 self._isolate_file = isolate_file | |
75 self._isolated_file = isolate_file + 'd' | |
76 self._connected = False | |
77 self._connect_event = threading.Event() | |
78 self._ip_address = None | |
79 self._config_vars = [] | |
80 self._dimensions = [] | |
81 self._rpc_timeout = None | |
82 self._otp = str(uuid.uuid1()) | |
Marc-Antoine Ruel (Google)
2015/01/30 18:00:21
eventually it'd be nice for this to be determinist
Mike Meade
2015/01/30 21:04:04
Maybe HOST_FQDN\CLIENT_NAME\SHA-1_LEGION_FILES?
T
| |
83 self._rpc = None | |
84 self._verbose = False | |
85 | |
86 @property | |
87 def connected(self): | |
88 """Return the value of self._connected.""" | |
89 return self._connected | |
90 | |
91 @property | |
92 def connect_event(self): | |
93 return self._connect_event | |
94 | |
95 @property | |
96 def rpc(self): | |
97 """Return the rpc object.""" | |
98 return self._rpc | |
99 | |
100 @property | |
101 def priority(self): | |
102 return self._priority | |
103 | |
104 @property | |
105 def rpc_idle_timeout(self): | |
106 return self._rpc_idle_timeout | |
107 | |
108 @property | |
109 def name(self): | |
110 return self._name | |
111 | |
112 @classmethod | |
113 def _IncreaseCount(cls): | |
114 """Increase the client_count parameter.""" | |
115 cls._client_count += 1 | |
116 | |
117 @classmethod | |
118 def _CreateName(cls): | |
119 """Create a name for this client. | |
120 | |
121 By default the name is "Client%s" where %s is the number of clients that | |
122 currently exist. | |
123 """ | |
124 return 'Client%d' % cls._client_count | |
125 | |
126 def SetPriority(self, priority): | |
Marc-Antoine Ruel (Google)
2015/01/30 18:00:21
Why not
@priority.setter
def priority(self, value)
Mike Meade
2015/01/30 21:04:04
The only reason I added the method like this was f
| |
127 """Sets the priority of the client task. | |
128 | |
129 Args: | |
130 priority: The priority to set. | |
131 """ | |
132 logging.debug('Setting %s priority to %s', self._name, priority) | |
133 self._priority = priority | |
134 | |
135 def SetVerbose(self): | |
136 """Set the client verbosity to debug.""" | |
137 logging.debug('Setting %s --verbosity', self._name) | |
138 self._verbose = True | |
139 | |
140 def AddConfigVars(self, key, value): | |
141 """Add a set of config vars to isolate.py. | |
142 | |
143 Args: | |
144 key: The config vars key. | |
145 value: The config vars value. | |
146 """ | |
147 logging.debug('Adding --config-var %s %s to %s', key, value, | |
148 self._name) | |
149 self._config_vars.append((key, value)) | |
150 | |
151 def AddDimension(self, key, value): | |
152 """Add a set of dimensions to swarming.py. | |
153 | |
154 Args: | |
155 key: The dimension key. | |
156 value: The dimension value. | |
157 """ | |
158 logging.debug('Adding --dimension %s %s to %s', key, value, | |
159 self._name) | |
160 self._dimensions.append((key, value)) | |
161 | |
162 def SetRPCTimeout(self, timeout): | |
163 """Sets the client's RPC timeout value. | |
164 | |
165 If the RPC server does not receive an RPC request within this time the | |
166 client controller will exit. | |
167 | |
168 Args: | |
169 timeout: The RPC server timeout in seconds. | |
170 """ | |
171 logging.debug('Setting %s RPC timeout to %s', self._name, timeout) | |
172 self._rpc_timeout = timeout | |
173 | |
174 def Create(self, wait=False, timeout=None): | |
175 """Create the client machine and wait for it to be created if specified. | |
176 | |
177 Args: | |
178 wait: True to block until created, False to return immediately. | |
179 timeout: The timeout to block before raising a ConnectionTimeoutError. | |
180 """ | |
181 logging.info('Creating %s', self._name) | |
182 self._connect_event.clear() | |
183 self._RegisterOnConnectCallback() | |
184 self._ExecuteIsolate() | |
185 self._ExecuteSwarming() | |
186 if wait: | |
187 self.WaitForConnection(timeout) | |
188 | |
189 def WaitForConnection(self, timeout=None): | |
190 """Connect to the client machine. | |
191 | |
192 This method waits for the client machine to register itself | |
193 with the discovery server. | |
194 | |
195 Args: | |
196 timeout: The timeout in seconds. | |
197 | |
198 Raises: | |
199 TimeoutError if the client doesn't connect in time. | |
200 """ | |
201 timeout = timeout or GetArgs().client_connection_timeout | |
202 msg = ('Waiting for %s to connect with a timeout of %d seconds' % | |
203 (self._name, timeout)) | |
204 logging.info(msg) | |
205 self._connect_event.wait(timeout) | |
206 if not self._connect_event.is_set(): | |
207 raise ConnectionTimeoutError() | |
208 | |
209 def Release(self): | |
210 """Quit the client's RPC server so it can release the machine.""" | |
211 if self._rpc is not None and self._connected: | |
212 logging.info('Releasing %s', self._name) | |
213 try: | |
214 self._rpc.Quit() | |
215 except (socket.error, xmlrpclib.Fault): | |
216 logging.error('Unable to connect to %s to call Quit', self._name) | |
217 self._rpc = None | |
218 self._connected = False | |
219 | |
220 def _ExecuteIsolate(self): | |
221 """Execute isolate.py with the given args.""" | |
222 cmd = [ | |
223 'python', | |
224 ISOLATE_PY, | |
225 'archive', | |
226 '--isolate=' + self._isolate_file, | |
227 '--isolated=' + self._isolated_file, | |
228 ] | |
229 | |
230 if GetArgs().isolate_server: | |
231 cmd += ['--isolate-server', GetArgs().isolate_server] | |
232 for key, value in self._config_vars: | |
233 cmd += ['--config-var', key, value] | |
234 | |
235 logging.debug('Running %s', ' '.join(cmd)) | |
236 if subprocess.call(cmd, stdout=subprocess.PIPE) != 0: | |
237 raise Error('Error calling isolate.py') | |
238 | |
239 def _ExecuteSwarming(self): | |
240 """Execute swarming.py with the vars.""" | |
241 cmd = [ | |
242 'python', | |
243 SWARMING_PY, | |
244 'trigger', | |
245 self._isolated_file, | |
246 '--priority=' + str(self._priority) | |
247 ] | |
248 | |
249 if GetArgs().isolate_server: | |
250 cmd += ['--isolate-server', GetArgs().isolate_server] | |
251 if GetArgs().swarming_server: | |
252 cmd += ['--swarming', GetArgs().swarming_server] | |
253 for key, value in self._dimensions: | |
254 cmd += ['--dimension', key, value] | |
255 | |
256 cmd += ['--', '--host', str(common_lib.MY_IP), '--otp', self._otp] | |
257 if self._rpc_timeout: | |
258 cmd += ['--idle-timeout', str(self._rpc_timeout)] | |
259 if self._verbose: | |
260 cmd += ['--verbose'] | |
261 | |
262 logging.debug('Running %s', ' '.join(cmd)) | |
263 if subprocess.call(cmd, stdout=subprocess.PIPE) != 0: | |
264 raise Error('Error calling swarming.py') | |
265 | |
266 def _RegisterOnConnectCallback(self): | |
267 """Register a callback with the discovery server. | |
268 | |
269 This callback is used to receive the client's IP address once it starts | |
270 and contacts the discovery server. | |
271 """ | |
272 self._discovery_server.RegisterClientCallback(self._otp, self._OnConnect) | |
273 | |
274 def _OnConnect(self, ip_address): | |
275 """The OnConnect callback method. | |
276 | |
277 This method receives the ip address received by the discovery server from | |
278 the client and sets the object's connected state to True. | |
279 """ | |
280 self._ip_address = ip_address | |
281 self._connected = True | |
282 self._rpc = client_rpc_server.RPCServer.Connect(self._ip_address) | |
283 logging.info('%s connected from %s', self._name, ip_address) | |
284 self._connect_event.set() | |
OLD | NEW |