blob: d8815e606235e015cb995aab050cda9d101a3a2d [file] [log] [blame]
mbligh67647152008-11-19 00:18:14 +00001# Copyright Martin J. Bligh, Google Inc 2008
2# Released under the GPL v2
3
4"""
5This class allows you to communicate with the frontend to submit jobs etc
6It is designed for writing more sophisiticated server-side control files that
7can recursively add and manage other jobs.
8
9We turn the JSON dictionaries into real objects that are more idiomatic
10
mblighc31e4022008-12-11 19:32:30 +000011For docs, see:
Aviv Keshet2c709f62013-05-07 12:52:15 -070012 http://www.chromium.org/chromium-os/testing/afe-rpc-infrastructure
mblighc31e4022008-12-11 19:32:30 +000013 http://docs.djangoproject.com/en/dev/ref/models/querysets/#queryset-api
mbligh67647152008-11-19 00:18:14 +000014"""
15
Richard Barnetteb9b37982016-05-20 19:23:39 -070016#pylint: disable=missing-docstring
17
Dan Shie8e0c052015-09-01 00:27:27 -070018import getpass
19import os
20import re
Dan Shie8e0c052015-09-01 00:27:27 -070021
mbligh67647152008-11-19 00:18:14 +000022import common
23from autotest_lib.frontend.afe import rpc_client_lib
Dan Shie8e0c052015-09-01 00:27:27 -070024from autotest_lib.client.common_lib import control_data
mbligh37eceaa2008-12-15 22:56:37 +000025from autotest_lib.client.common_lib import global_config
mbligh67647152008-11-19 00:18:14 +000026from autotest_lib.client.common_lib import utils
Dan Shie8e0c052015-09-01 00:27:27 -070027from autotest_lib.client.common_lib.cros.graphite import autotest_stats
Scott Zawalski63470dd2012-09-05 00:49:43 -040028from autotest_lib.tko import db
29
30
mbligh4e576612008-12-22 14:56:36 +000031try:
32 from autotest_lib.server.site_common import site_utils as server_utils
33except:
34 from autotest_lib.server import utils as server_utils
35form_ntuples_from_machines = server_utils.form_ntuples_from_machines
mbligh67647152008-11-19 00:18:14 +000036
mbligh37eceaa2008-12-15 22:56:37 +000037GLOBAL_CONFIG = global_config.global_config
38DEFAULT_SERVER = 'autotest'
39
Dan Shie8e0c052015-09-01 00:27:27 -070040_tko_timer = autotest_stats.Timer('tko')
41
mbligh67647152008-11-19 00:18:14 +000042def dump_object(header, obj):
43 """
44 Standard way to print out the frontend objects (eg job, host, acl, label)
45 in a human-readable fashion for debugging
46 """
47 result = header + '\n'
48 for key in obj.hash:
49 if key == 'afe' or key == 'hash':
50 continue
51 result += '%20s: %s\n' % (key, obj.hash[key])
52 return result
53
54
mbligh5280e3b2008-12-22 14:39:28 +000055class RpcClient(object):
mbligh67647152008-11-19 00:18:14 +000056 """
mbligh451ede12009-02-12 21:54:03 +000057 Abstract RPC class for communicating with the autotest frontend
58 Inherited for both TKO and AFE uses.
mbligh67647152008-11-19 00:18:14 +000059
mbligh1ef218d2009-08-03 16:57:56 +000060 All the constructors go in the afe / tko class.
mbligh451ede12009-02-12 21:54:03 +000061 Manipulating methods go in the object classes themselves
mbligh67647152008-11-19 00:18:14 +000062 """
mbligh99b24f42009-06-08 16:45:55 +000063 def __init__(self, path, user, server, print_log, debug, reply_debug):
mbligh67647152008-11-19 00:18:14 +000064 """
mbligh451ede12009-02-12 21:54:03 +000065 Create a cached instance of a connection to the frontend
mbligh67647152008-11-19 00:18:14 +000066
67 user: username to connect as
mbligh451ede12009-02-12 21:54:03 +000068 server: frontend server to connect to
mbligh67647152008-11-19 00:18:14 +000069 print_log: pring a logging message to stdout on every operation
70 debug: print out all RPC traffic
71 """
Dan Shiff78f112015-06-12 13:34:02 -070072 if not user and utils.is_in_container():
73 user = GLOBAL_CONFIG.get_config_value('SSP', 'user', default=None)
mblighc31e4022008-12-11 19:32:30 +000074 if not user:
mblighdb59e3c2009-11-21 01:45:18 +000075 user = getpass.getuser()
mbligh451ede12009-02-12 21:54:03 +000076 if not server:
mbligh475f7762009-01-30 00:34:04 +000077 if 'AUTOTEST_WEB' in os.environ:
mbligh451ede12009-02-12 21:54:03 +000078 server = os.environ['AUTOTEST_WEB']
mbligh475f7762009-01-30 00:34:04 +000079 else:
mbligh451ede12009-02-12 21:54:03 +000080 server = GLOBAL_CONFIG.get_config_value('SERVER', 'hostname',
81 default=DEFAULT_SERVER)
82 self.server = server
mbligh67647152008-11-19 00:18:14 +000083 self.user = user
84 self.print_log = print_log
85 self.debug = debug
mbligh99b24f42009-06-08 16:45:55 +000086 self.reply_debug = reply_debug
Scott Zawalski347aaf42012-04-03 16:33:00 -040087 headers = {'AUTHORIZATION': self.user}
88 rpc_server = 'http://' + server + path
mbligh1354c9d2008-12-22 14:56:13 +000089 if debug:
90 print 'SERVER: %s' % rpc_server
91 print 'HEADERS: %s' % headers
mbligh67647152008-11-19 00:18:14 +000092 self.proxy = rpc_client_lib.get_proxy(rpc_server, headers=headers)
93
94
95 def run(self, call, **dargs):
96 """
97 Make a RPC call to the AFE server
98 """
99 rpc_call = getattr(self.proxy, call)
100 if self.debug:
101 print 'DEBUG: %s %s' % (call, dargs)
mbligh451ede12009-02-12 21:54:03 +0000102 try:
mbligh99b24f42009-06-08 16:45:55 +0000103 result = utils.strip_unicode(rpc_call(**dargs))
104 if self.reply_debug:
105 print result
106 return result
mbligh451ede12009-02-12 21:54:03 +0000107 except Exception:
mbligh451ede12009-02-12 21:54:03 +0000108 raise
mbligh67647152008-11-19 00:18:14 +0000109
110
111 def log(self, message):
112 if self.print_log:
113 print message
114
115
jamesrenc3940222010-02-19 21:57:37 +0000116class Planner(RpcClient):
117 def __init__(self, user=None, server=None, print_log=True, debug=False,
118 reply_debug=False):
119 super(Planner, self).__init__(path='/planner/server/rpc/',
120 user=user,
121 server=server,
122 print_log=print_log,
123 debug=debug,
124 reply_debug=reply_debug)
125
126
mbligh5280e3b2008-12-22 14:39:28 +0000127class TKO(RpcClient):
mbligh99b24f42009-06-08 16:45:55 +0000128 def __init__(self, user=None, server=None, print_log=True, debug=False,
129 reply_debug=False):
Scott Zawalski347aaf42012-04-03 16:33:00 -0400130 super(TKO, self).__init__(path='/new_tko/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000131 user=user,
132 server=server,
133 print_log=print_log,
134 debug=debug,
135 reply_debug=reply_debug)
Scott Zawalski63470dd2012-09-05 00:49:43 -0400136 self._db = None
137
138
Dan Shie8e0c052015-09-01 00:27:27 -0700139 @_tko_timer.decorate
Scott Zawalski63470dd2012-09-05 00:49:43 -0400140 def get_job_test_statuses_from_db(self, job_id):
141 """Get job test statuses from the database.
142
143 Retrieve a set of fields from a job that reflect the status of each test
144 run within a job.
145 fields retrieved: status, test_name, reason, test_started_time,
146 test_finished_time, afe_job_id, job_owner, hostname.
147
148 @param job_id: The afe job id to look up.
149 @returns a TestStatus object of the resulting information.
150 """
151 if self._db is None:
Dan Shie8e0c052015-09-01 00:27:27 -0700152 self._db = db.db()
Fang Deng5c508332014-03-19 10:26:00 -0700153 fields = ['status', 'test_name', 'subdir', 'reason',
154 'test_started_time', 'test_finished_time', 'afe_job_id',
155 'job_owner', 'hostname', 'job_tag']
Scott Zawalski63470dd2012-09-05 00:49:43 -0400156 table = 'tko_test_view_2'
157 where = 'job_tag like "%s-%%"' % job_id
158 test_status = []
159 # Run commit before we query to ensure that we are pulling the latest
160 # results.
161 self._db.commit()
162 for entry in self._db.select(','.join(fields), table, (where, None)):
163 status_dict = {}
164 for key,value in zip(fields, entry):
165 # All callers expect values to be a str object.
166 status_dict[key] = str(value)
167 # id is used by TestStatus to uniquely identify each Test Status
168 # obj.
169 status_dict['id'] = [status_dict['reason'], status_dict['hostname'],
170 status_dict['test_name']]
171 test_status.append(status_dict)
172
173 return [TestStatus(self, e) for e in test_status]
mblighc31e4022008-12-11 19:32:30 +0000174
175
176 def get_status_counts(self, job, **data):
177 entries = self.run('get_status_counts',
mbligh1ef218d2009-08-03 16:57:56 +0000178 group_by=['hostname', 'test_name', 'reason'],
mblighc31e4022008-12-11 19:32:30 +0000179 job_tag__startswith='%s-' % job, **data)
mbligh5280e3b2008-12-22 14:39:28 +0000180 return [TestStatus(self, e) for e in entries['groups']]
mblighc31e4022008-12-11 19:32:30 +0000181
182
Richard Barnette260cbd02016-10-06 12:23:28 -0700183class _StableVersionMap(object):
184 """
185 A mapping from board names to strings naming software versions.
186
187 The mapping is meant to allow finding a nominally "stable" version
188 of software associated with a given board. The mapping identifies
189 specific versions of software that should be installed during
190 operations such as repair.
191
192 Conceptually, there are multiple version maps, each handling
193 different types of image. For instance, a single board may have
194 both a stable OS image (e.g. for CrOS), and a separate stable
195 firmware image.
196
197 Each different type of image requires a certain amount of special
198 handling, implemented by a subclass of `StableVersionMap`. The
199 subclasses take care of pre-processing of arguments, delegating
200 actual RPC calls to this superclass.
201
202 @property _afe AFE object through which to make the actual RPC
203 calls.
204 @property _android Value of the `android` parameter to be passed
205 when calling the `get_stable_version` RPC.
206 """
207
208 # DEFAULT_BOARD - The stable_version RPC API recognizes this special
209 # name as a mapping to use when no specific mapping for a board is
210 # present. This default mapping is only allowed for CrOS image
211 # types; other image type subclasses exclude it.
212 #
213 # TODO(jrbarnette): This value is copied from
214 # site_utils.stable_version_utils, because if we import that
215 # module here, it breaks unit tests. Something about the Django
216 # setup...
217 DEFAULT_BOARD = 'DEFAULT'
218
219
220 def __init__(self, afe, android):
221 self._afe = afe
222 self._android = android
223
224
225 def get_all_versions(self):
226 """
227 Get all mappings in the stable versions table.
228
229 Extracts the full content of the `stable_version` table
230 in the AFE database, and returns it as a dictionary
231 mapping board names to version strings.
232
233 @return A dictionary mapping board names to version strings.
234 """
235 return self._afe.run('get_all_stable_versions')
236
237
238 def get_version(self, board):
239 """
240 Get the mapping of one board in the stable versions table.
241
242 Look up and return the version mapped to the given board in the
243 `stable_versions` table in the AFE database.
244
245 @param board The board to be looked up.
246
247 @return The version mapped for the given board.
248 """
249 return self._afe.run('get_stable_version',
250 board=board, android=self._android)
251
252
253 def set_version(self, board, version):
254 """
255 Change the mapping of one board in the stable versions table.
256
257 Set the mapping in the `stable_versions` table in the AFE
258 database for the given board to the given version.
259
260 @param board The board to be updated.
261 @param version The new version to be assigned to the board.
262 """
263 self._afe.run('set_stable_version',
264 version=version, board=board)
265
266
267 def delete_version(self, board):
268 """
269 Remove the mapping of one board in the stable versions table.
270
271 Remove the mapping in the `stable_versions` table in the AFE
272 database for the given board.
273
274 @param board The board to be updated.
275 """
276 self._afe.run('delete_stable_version', board=board)
277
278
279class _OSVersionMap(_StableVersionMap):
280 """
281 Abstract stable version mapping for full OS images of various types.
282 """
283
284 def get_all_versions(self):
285 # TODO(jrbarnette): We exclude FAFT version mappings, but the
286 # returned dict doesn't distinguish CrOS boards from Android
287 # boards; both will be present, and the subclass can't
288 # distinguish them.
289 #
290 # Ultimately, the right fix is to move knowledge of image type
291 # over to the RPC server side.
292 #
293 versions = super(_OSVersionMap, self).get_all_versions()
294 for board in versions.keys():
295 if '/' in board:
296 del versions[board]
297 return versions
298
299
300class _CrosVersionMap(_OSVersionMap):
301 """
302 Stable version mapping for Chrome OS release images.
303
304 This class manages a mapping of Chrome OS board names to known-good
305 release (or canary) images. The images selected can be installed on
306 DUTs during repair tasks, as a way of getting a DUT into a known
307 working state.
308 """
309
310 def __init__(self, afe):
311 super(_CrosVersionMap, self).__init__(afe, False)
312
313
314 def get_version(self, board):
315 version = super(_CrosVersionMap, self).get_version(board)
316 build_pattern = GLOBAL_CONFIG.get_config_value(
317 'CROS', 'stable_build_pattern')
318 return build_pattern % (board, version)
319
320
321class _AndroidVersionMap(_OSVersionMap):
322 """
323 Stable version mapping for Android release images.
324
325 This class manages a mapping of Android/Brillo board names to
326 known-good images.
327 """
328
329 def __init__(self, afe):
330 super(_AndroidVersionMap, self).__init__(afe, True)
331
332
333 def get_all_versions(self):
334 versions = super(_AndroidVersionMap, self).get_all_versions()
335 del versions[self.DEFAULT_BOARD]
336 return versions
337
338
339class _FAFTVersionMap(_StableVersionMap):
340 """
341 Stable version mapping for firmware versions used in FAFT repair.
342
343 When DUTs used for FAFT fail repair, stable firmware may need to be
344 flashed directly from original tarballs. The FAFT firmware version
345 mapping finds the appropriate tarball for a given board.
346 """
347
348 # _SUFFIX - FAFT firmware versions are recorded in the
349 # stable_versions table by appending this value to the board
350 # name to construct a pseudo-board name that maps to a firmware
351 # version string.
352 #
353 _SUFFIX = '/firmware'
354
355 def __init__(self, afe):
356 super(_FAFTVersionMap, self).__init__(afe, False)
357
358
359 def get_all_versions(self):
360 # Get all the mappings from the AFE, extract just the FAFT
361 # firmware mappings, and replace the pseudo-board name keys with
362 # the real board names.
363 #
364 all_versions = super(
365 _FAFTVersionMap, self).get_all_versions()
366 return {
367 board[0 : -len(self._SUFFIX)]: all_versions[board]
368 for board in all_versions.keys()
369 if board.endswith(self._SUFFIX)
370 }
371
372
373 def get_version(self, board):
374 # If there's no firmware mapping for `board`, the lookup will
375 # return the default CrOS version mapping. To eliminate that
376 # case, we require a '/' character in the version, since CrOS
377 # versions won't match that.
378 #
379 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
380 # the right fix is to move handling to the RPC server side.
381 #
382 board += self._SUFFIX
383 version = super(_FAFTVersionMap, self).get_version(board)
384 return version if '/' in version else None
385
386
387 def set_version(self, board, version):
388 super(_FAFTVersionMap, self).set_version(
389 board + self._SUFFIX, version)
390
391
392 def delete_version(self, board):
393 super(_FAFTVersionMap, self).delete_version(
394 board + self._SUFFIX)
395
396
mbligh5280e3b2008-12-22 14:39:28 +0000397class AFE(RpcClient):
mbligh17c75e62009-06-08 16:18:21 +0000398 def __init__(self, user=None, server=None, print_log=True, debug=False,
mbligh99b24f42009-06-08 16:45:55 +0000399 reply_debug=False, job=None):
mbligh17c75e62009-06-08 16:18:21 +0000400 self.job = job
Scott Zawalski347aaf42012-04-03 16:33:00 -0400401 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000402 user=user,
403 server=server,
404 print_log=print_log,
405 debug=debug,
406 reply_debug=reply_debug)
mblighc31e4022008-12-11 19:32:30 +0000407
mbligh1ef218d2009-08-03 16:57:56 +0000408
Richard Barnette260cbd02016-10-06 12:23:28 -0700409 # Known image types for stable version mapping objects.
410 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
411 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
412 # ANDROID_IMAGE_TYPE - Mappings for Android images.
413 #
414 CROS_IMAGE_TYPE = 'cros'
415 FAFT_IMAGE_TYPE = 'faft'
416 ANDROID_IMAGE_TYPE = 'android'
417
418 _IMAGE_MAPPING_CLASSES = {
419 CROS_IMAGE_TYPE: _CrosVersionMap,
420 FAFT_IMAGE_TYPE: _FAFTVersionMap,
421 ANDROID_IMAGE_TYPE: _AndroidVersionMap
422 }
423
424
425 def get_stable_version_map(self, image_type):
426 """
427 Return a stable version mapping for the given image type.
428
429 @return An object mapping board names to version strings for
430 software of the given image type.
431 """
432 return self._IMAGE_MAPPING_CLASSES[image_type](self)
433
434
mbligh67647152008-11-19 00:18:14 +0000435 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000436 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000437 statuses = self.run('get_static_data')['host_statuses']
438 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000439 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000440 if live == False:
441 return dead_statuses
442 else:
443 return statuses
444
445
mbligh71094012009-12-19 05:35:21 +0000446 @staticmethod
447 def _dict_for_host_query(hostnames=(), status=None, label=None):
448 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000449 if hostnames:
450 query_args['hostname__in'] = hostnames
451 if status:
452 query_args['status'] = status
453 if label:
454 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000455 return query_args
456
457
458 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
459 query_args = dict(dargs)
460 query_args.update(self._dict_for_host_query(hostnames=hostnames,
461 status=status,
462 label=label))
463 hosts = self.run('get_hosts', **query_args)
464 return [Host(self, h) for h in hosts]
465
466
467 def get_hostnames(self, status=None, label=None, **dargs):
468 """Like get_hosts() but returns hostnames instead of Host objects."""
469 # This implementation can be replaced with a more efficient one
470 # that does not query for entire host objects in the future.
471 return [host_obj.hostname for host_obj in
472 self.get_hosts(status=status, label=label, **dargs)]
473
474
475 def reverify_hosts(self, hostnames=(), status=None, label=None):
476 query_args = dict(locked=False,
477 aclgroup__users__login=self.user)
478 query_args.update(self._dict_for_host_query(hostnames=hostnames,
479 status=status,
480 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000481 return self.run('reverify_hosts', **query_args)
482
483
mbligh67647152008-11-19 00:18:14 +0000484 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000485 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000486 return self.get_hosts(id=id)[0]
487
488
MK Ryuacf35922014-10-03 14:56:49 -0700489 def get_host_attribute(self, attr, **dargs):
490 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
491 return [HostAttribute(self, a) for a in host_attrs]
492
493
Chris Masone8abb6fc2012-01-31 09:27:36 -0800494 def set_host_attribute(self, attr, val, **dargs):
495 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
496
497
mbligh67647152008-11-19 00:18:14 +0000498 def get_labels(self, **dargs):
499 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000500 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000501
502
503 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000504 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000505 return self.get_labels(id=id)[0]
506
507
508 def get_acls(self, **dargs):
509 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000510 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000511
512
513 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000514 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000515 return self.get_acls(id=id)[0]
516
517
mbligh54459c72009-01-21 19:26:44 +0000518 def get_users(self, **dargs):
519 users = self.run('get_users', **dargs)
520 return [User(self, u) for u in users]
521
522
mbligh1354c9d2008-12-22 14:56:13 +0000523 def generate_control_file(self, tests, **dargs):
524 ret = self.run('generate_control_file', tests=tests, **dargs)
525 return ControlFile(self, ret)
526
527
mbligh67647152008-11-19 00:18:14 +0000528 def get_jobs(self, summary=False, **dargs):
529 if summary:
530 jobs_data = self.run('get_jobs_summary', **dargs)
531 else:
532 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000533 jobs = []
534 for j in jobs_data:
535 job = Job(self, j)
536 # Set up some extra information defaults
537 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
538 job.platform_results = {}
539 job.platform_reasons = {}
540 jobs.append(job)
541 return jobs
mbligh67647152008-11-19 00:18:14 +0000542
543
544 def get_host_queue_entries(self, **data):
545 entries = self.run('get_host_queue_entries', **data)
mblighf9e35862009-02-26 01:03:11 +0000546 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000547
548 # Sadly, get_host_queue_entries doesn't return platforms, we have
549 # to get those back from an explicit get_hosts queury, then patch
550 # the new host objects back into the host list.
551 hostnames = [s.host.hostname for s in job_statuses if s.host]
552 host_hash = {}
553 for host in self.get_hosts(hostname__in=hostnames):
554 host_hash[host.hostname] = host
555 for status in job_statuses:
556 if status.host:
Fang Deng97dafbc2015-04-23 23:06:18 -0700557 status.host = host_hash.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000558 # filter job statuses that have either host or meta_host
559 return [status for status in job_statuses if (status.host or
560 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000561
562
MK Ryu1b2d7f92015-02-24 17:45:02 -0800563 def get_special_tasks(self, **data):
564 tasks = self.run('get_special_tasks', **data)
565 return [SpecialTask(self, t) for t in tasks]
566
567
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700568 def get_host_special_tasks(self, host_id, **data):
569 tasks = self.run('get_host_special_tasks',
570 host_id=host_id, **data)
571 return [SpecialTask(self, t) for t in tasks]
572
573
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700574 def get_host_status_task(self, host_id, end_time):
575 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700576 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700577 return SpecialTask(self, task) if task else None
578
579
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700580 def get_host_diagnosis_interval(self, host_id, end_time, success):
581 return self.run('get_host_diagnosis_interval',
582 host_id=host_id, end_time=end_time,
583 success=success)
584
585
mbligh67647152008-11-19 00:18:14 +0000586 def create_job(self, control_file, name=' ', priority='Medium',
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700587 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT, **dargs):
mbligh67647152008-11-19 00:18:14 +0000588 id = self.run('create_job', name=name, priority=priority,
589 control_file=control_file, control_type=control_type, **dargs)
590 return self.get_jobs(id=id)[0]
591
592
Simran Basi01984f52015-10-12 15:36:45 -0700593 def abort_jobs(self, jobs):
594 """Abort a list of jobs.
595
596 Already completed jobs will not be affected.
597
598 @param jobs: List of job ids to abort.
599 """
600 for job in jobs:
601 self.run('abort_host_queue_entries', job_id=job)
602
603
Kevin Cheng19521982016-09-22 12:27:23 -0700604 def get_hosts_by_attribute(self, attribute, value):
605 """
606 Get the list of hosts that share the same host attribute value.
607
608 @param attribute: String of the host attribute to check.
609 @param value: String of the value that is shared between hosts.
610
611 @returns List of hostnames that all have the same host attribute and
612 value.
613 """
614 return self.run('get_hosts_by_attribute',
615 attribute=attribute, value=value)
616
617
618 def lock_host(self, host, lock_reason, fail_if_locked=False):
619 """
620 Lock the given host with the given lock reason.
621
622 Locking a host that's already locked using the 'modify_hosts' rpc
623 will raise an exception. That's why fail_if_locked exists so the
624 caller can determine if the lock succeeded or failed. This will
625 save every caller from wrapping lock_host in a try-except.
626
627 @param host: hostname of host to lock.
628 @param lock_reason: Reason for locking host.
629 @param fail_if_locked: Return False if host is already locked.
630
631 @returns Boolean, True if lock was successful, False otherwise.
632 """
633 try:
634 self.run('modify_hosts',
635 host_filter_data={'hostname': host},
636 update_data={'locked': True,
637 'lock_reason': lock_reason})
638 except Exception:
639 return not fail_if_locked
640 return True
641
642
643 def unlock_hosts(self, locked_hosts):
644 """
645 Unlock the hosts.
646
647 Unlocking a host that's already unlocked will do nothing so we don't
648 need any special try-except clause here.
649
650 @param locked_hosts: List of hostnames of hosts to unlock.
651 """
652 self.run('modify_hosts',
653 host_filter_data={'hostname__in': locked_hosts},
654 update_data={'locked': False,
655 'lock_reason': ''})
656
657
mbligh5280e3b2008-12-22 14:39:28 +0000658class TestResults(object):
659 """
660 Container class used to hold the results of the tests for a job
661 """
662 def __init__(self):
663 self.good = []
664 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000665 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000666
667
668 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000669 if result.complete_count > result.pass_count:
670 self.fail.append(result)
671 elif result.incomplete_count > 0:
672 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000673 else:
mbligh451ede12009-02-12 21:54:03 +0000674 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000675
676
677class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000678 """
679 Generic object used to construct python objects from rpc calls
680 """
681 def __init__(self, afe, hash):
682 self.afe = afe
683 self.hash = hash
684 self.__dict__.update(hash)
685
686
687 def __str__(self):
688 return dump_object(self.__repr__(), self)
689
690
mbligh1354c9d2008-12-22 14:56:13 +0000691class ControlFile(RpcObject):
692 """
693 AFE control file object
694
695 Fields: synch_count, dependencies, control_file, is_server
696 """
697 def __repr__(self):
698 return 'CONTROL FILE: %s' % self.control_file
699
700
mbligh5280e3b2008-12-22 14:39:28 +0000701class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000702 """
703 AFE label object
704
705 Fields:
706 name, invalid, platform, kernel_config, id, only_if_needed
707 """
708 def __repr__(self):
709 return 'LABEL: %s' % self.name
710
711
712 def add_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800713 return self.afe.run('label_add_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000714
715
716 def remove_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800717 return self.afe.run('label_remove_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000718
719
mbligh5280e3b2008-12-22 14:39:28 +0000720class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000721 """
722 AFE acl object
723
724 Fields:
725 users, hosts, description, name, id
726 """
727 def __repr__(self):
728 return 'ACL: %s' % self.name
729
730
731 def add_hosts(self, hosts):
732 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
733 return self.afe.run('acl_group_add_hosts', self.id, hosts)
734
735
736 def remove_hosts(self, hosts):
737 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
738 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
739
740
mbligh54459c72009-01-21 19:26:44 +0000741 def add_users(self, users):
742 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
743 return self.afe.run('acl_group_add_users', id=self.name, users=users)
744
745
mbligh5280e3b2008-12-22 14:39:28 +0000746class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000747 """
748 AFE job object
749
750 Fields:
751 name, control_file, control_type, synch_count, reboot_before,
752 run_verify, priority, email_list, created_on, dependencies,
753 timeout, owner, reboot_after, id
754 """
755 def __repr__(self):
756 return 'JOB: %s' % self.id
757
758
mbligh5280e3b2008-12-22 14:39:28 +0000759class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000760 """
761 AFE job_status object
762
763 Fields:
764 status, complete, deleted, meta_host, host, active, execution_subdir, id
765 """
766 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800767 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000768 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700769 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000770 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000771
772
773 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000774 if self.host and self.host.hostname:
775 hostname = self.host.hostname
776 else:
777 hostname = 'None'
778 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000779
780
MK Ryu1b2d7f92015-02-24 17:45:02 -0800781class SpecialTask(RpcObject):
782 """
783 AFE special task object
784 """
785 def __init__(self, afe, hash):
786 super(SpecialTask, self).__init__(afe, hash)
787 self.host = Host(afe, self.host)
788
789
790 def __repr__(self):
791 return 'SPECIAL TASK: %s' % self.id
792
793
mbligh5280e3b2008-12-22 14:39:28 +0000794class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000795 """
796 AFE host object
797
798 Fields:
799 status, lock_time, locked_by, locked, hostname, invalid,
800 synch_id, labels, platform, protection, dirty, id
801 """
802 def __repr__(self):
803 return 'HOST OBJECT: %s' % self.hostname
804
805
806 def show(self):
807 labels = list(set(self.labels) - set([self.platform]))
808 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
809 self.locked, self.platform,
810 ', '.join(labels))
811
812
mbligh54459c72009-01-21 19:26:44 +0000813 def delete(self):
814 return self.afe.run('delete_host', id=self.id)
815
816
mbligh6463c4b2009-01-30 00:33:37 +0000817 def modify(self, **dargs):
818 return self.afe.run('modify_host', id=self.id, **dargs)
819
820
mbligh67647152008-11-19 00:18:14 +0000821 def get_acls(self):
822 return self.afe.get_acls(hosts__hostname=self.hostname)
823
824
825 def add_acl(self, acl_name):
826 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
827 return self.afe.run('acl_group_add_hosts', id=acl_name,
828 hosts=[self.hostname])
829
830
831 def remove_acl(self, acl_name):
832 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
833 return self.afe.run('acl_group_remove_hosts', id=acl_name,
834 hosts=[self.hostname])
835
836
837 def get_labels(self):
838 return self.afe.get_labels(host__hostname__in=[self.hostname])
839
840
841 def add_labels(self, labels):
842 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
843 return self.afe.run('host_add_labels', id=self.id, labels=labels)
844
845
846 def remove_labels(self, labels):
847 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
848 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000849
850
mbligh54459c72009-01-21 19:26:44 +0000851class User(RpcObject):
852 def __repr__(self):
853 return 'USER: %s' % self.login
854
855
mbligh5280e3b2008-12-22 14:39:28 +0000856class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000857 """
858 TKO test status object
859
860 Fields:
861 test_idx, hostname, testname, id
862 complete_count, incomplete_count, group_count, pass_count
863 """
864 def __repr__(self):
865 return 'TEST STATUS: %s' % self.id
866
867
MK Ryuacf35922014-10-03 14:56:49 -0700868class HostAttribute(RpcObject):
869 """
870 AFE host attribute object
871
872 Fields:
873 id, host, attribute, value
874 """
875 def __repr__(self):
876 return 'HOST ATTRIBUTE %d' % self.id