blob: c0d8aac88926a65d897fcb555cb838343d1c0cae [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):
Richard Barnettee50453e2016-10-10 16:43:44 -0700285 # TODO(jrbarnette): We exclude non-OS (i.e. firmware) version
286 # mappings, but the returned dict doesn't distinguish CrOS
287 # boards from Android boards; both will be present, and the
288 # subclass can't distinguish them.
Richard Barnette260cbd02016-10-06 12:23:28 -0700289 #
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
Richard Barnettee50453e2016-10-10 16:43:44 -0700339class _SuffixHackVersionMap(_StableVersionMap):
Richard Barnette260cbd02016-10-06 12:23:28 -0700340 """
Richard Barnettee50453e2016-10-10 16:43:44 -0700341 Abstract super class for mappings using a pseudo-board name.
Richard Barnette260cbd02016-10-06 12:23:28 -0700342
Richard Barnettee50453e2016-10-10 16:43:44 -0700343 For non-OS image type mappings, we look them up in the
344 `stable_versions` table by constructing a "pseudo-board" from the
345 real board name plus a suffix string that identifies the image type.
346 So, for instance the name "lulu/firmware" is used to look up the
347 FAFT firmware version for lulu boards.
Richard Barnette260cbd02016-10-06 12:23:28 -0700348 """
349
Richard Barnettee50453e2016-10-10 16:43:44 -0700350 # _SUFFIX - The suffix used in constructing the "pseudo-board"
351 # lookup key. Each subclass must define this value for itself.
Richard Barnette260cbd02016-10-06 12:23:28 -0700352 #
Richard Barnettee50453e2016-10-10 16:43:44 -0700353 _SUFFIX = None
Richard Barnette260cbd02016-10-06 12:23:28 -0700354
355 def __init__(self, afe):
Richard Barnettee50453e2016-10-10 16:43:44 -0700356 super(_SuffixHackVersionMap, self).__init__(afe, False)
Richard Barnette260cbd02016-10-06 12:23:28 -0700357
358
359 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700360 # Get all the mappings from the AFE, extract just the mappings
361 # with our suffix, and replace the pseudo-board name keys with
Richard Barnette260cbd02016-10-06 12:23:28 -0700362 # the real board names.
363 #
364 all_versions = super(
Richard Barnettee50453e2016-10-10 16:43:44 -0700365 _SuffixHackVersionMap, self).get_all_versions()
Richard Barnette260cbd02016-10-06 12:23:28 -0700366 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):
Richard Barnettee50453e2016-10-10 16:43:44 -0700374 board += self._SUFFIX
375 return super(_SuffixHackVersionMap, self).get_version(board)
376
377
378 def set_version(self, board, version):
379 board += self._SUFFIX
380 super(_SuffixHackVersionMap, self).set_version(board, version)
381
382
383 def delete_version(self, board):
384 board += self._SUFFIX
385 super(_SuffixHackVersionMap, self).delete_version(board)
386
387
388class _FAFTVersionMap(_SuffixHackVersionMap):
389 """
390 Stable version mapping for firmware versions used in FAFT repair.
391
392 When DUTs used for FAFT fail repair, stable firmware may need to be
393 flashed directly from original tarballs. The FAFT firmware version
394 mapping finds the appropriate tarball for a given board.
395 """
396
397 _SUFFIX = '/firmware'
398
399 def get_version(self, board):
400 # If there's no mapping for `board`, the lookup will return the
401 # default CrOS version mapping. To eliminate that case, we
402 # require a '/' character in the version, since CrOS versions
403 # won't match that.
Richard Barnette260cbd02016-10-06 12:23:28 -0700404 #
405 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
406 # the right fix is to move handling to the RPC server side.
407 #
Richard Barnette260cbd02016-10-06 12:23:28 -0700408 version = super(_FAFTVersionMap, self).get_version(board)
409 return version if '/' in version else None
410
411
Richard Barnettee50453e2016-10-10 16:43:44 -0700412class _FirmwareVersionMap(_SuffixHackVersionMap):
413 """
414 Stable version mapping for firmware supplied in Chrome OS images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700415
Richard Barnettee50453e2016-10-10 16:43:44 -0700416 A Chrome OS image bundles a version of the firmware that the
417 device should update to when the OS version is installed during
418 AU.
Richard Barnette260cbd02016-10-06 12:23:28 -0700419
Richard Barnettee50453e2016-10-10 16:43:44 -0700420 Test images suppress the firmware update during AU. Instead, during
421 repair and verify we check installed firmware on a DUT, compare it
422 against the stable version mapping for the board, and update when
423 the DUT is out-of-date.
424 """
425
426 _SUFFIX = '/rwfw'
427
428 def get_version(self, board):
429 # If there's no mapping for `board`, the lookup will return the
430 # default CrOS version mapping. To eliminate that case, we
431 # require the version start with "Google_", since CrOS versions
432 # won't match that.
433 #
434 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
435 # the right fix is to move handling to the RPC server side.
436 #
437 version = super(_FirmwareVersionMap, self).get_version(board)
438 return version if version.startswith('Google_') else None
Richard Barnette260cbd02016-10-06 12:23:28 -0700439
440
mbligh5280e3b2008-12-22 14:39:28 +0000441class AFE(RpcClient):
mbligh17c75e62009-06-08 16:18:21 +0000442 def __init__(self, user=None, server=None, print_log=True, debug=False,
mbligh99b24f42009-06-08 16:45:55 +0000443 reply_debug=False, job=None):
mbligh17c75e62009-06-08 16:18:21 +0000444 self.job = job
Scott Zawalski347aaf42012-04-03 16:33:00 -0400445 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000446 user=user,
447 server=server,
448 print_log=print_log,
449 debug=debug,
450 reply_debug=reply_debug)
mblighc31e4022008-12-11 19:32:30 +0000451
mbligh1ef218d2009-08-03 16:57:56 +0000452
Richard Barnette260cbd02016-10-06 12:23:28 -0700453 # Known image types for stable version mapping objects.
454 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
455 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
Richard Barnettee50453e2016-10-10 16:43:44 -0700456 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700457 # ANDROID_IMAGE_TYPE - Mappings for Android images.
458 #
459 CROS_IMAGE_TYPE = 'cros'
460 FAFT_IMAGE_TYPE = 'faft'
Richard Barnettee50453e2016-10-10 16:43:44 -0700461 FIRMWARE_IMAGE_TYPE = 'firmware'
Richard Barnette260cbd02016-10-06 12:23:28 -0700462 ANDROID_IMAGE_TYPE = 'android'
463
464 _IMAGE_MAPPING_CLASSES = {
465 CROS_IMAGE_TYPE: _CrosVersionMap,
466 FAFT_IMAGE_TYPE: _FAFTVersionMap,
Richard Barnettee50453e2016-10-10 16:43:44 -0700467 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
Richard Barnette260cbd02016-10-06 12:23:28 -0700468 ANDROID_IMAGE_TYPE: _AndroidVersionMap
469 }
470
471
472 def get_stable_version_map(self, image_type):
473 """
474 Return a stable version mapping for the given image type.
475
476 @return An object mapping board names to version strings for
477 software of the given image type.
478 """
479 return self._IMAGE_MAPPING_CLASSES[image_type](self)
480
481
mbligh67647152008-11-19 00:18:14 +0000482 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000483 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000484 statuses = self.run('get_static_data')['host_statuses']
485 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000486 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000487 if live == False:
488 return dead_statuses
489 else:
490 return statuses
491
492
mbligh71094012009-12-19 05:35:21 +0000493 @staticmethod
494 def _dict_for_host_query(hostnames=(), status=None, label=None):
495 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000496 if hostnames:
497 query_args['hostname__in'] = hostnames
498 if status:
499 query_args['status'] = status
500 if label:
501 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000502 return query_args
503
504
505 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
506 query_args = dict(dargs)
507 query_args.update(self._dict_for_host_query(hostnames=hostnames,
508 status=status,
509 label=label))
510 hosts = self.run('get_hosts', **query_args)
511 return [Host(self, h) for h in hosts]
512
513
514 def get_hostnames(self, status=None, label=None, **dargs):
515 """Like get_hosts() but returns hostnames instead of Host objects."""
516 # This implementation can be replaced with a more efficient one
517 # that does not query for entire host objects in the future.
518 return [host_obj.hostname for host_obj in
519 self.get_hosts(status=status, label=label, **dargs)]
520
521
522 def reverify_hosts(self, hostnames=(), status=None, label=None):
523 query_args = dict(locked=False,
524 aclgroup__users__login=self.user)
525 query_args.update(self._dict_for_host_query(hostnames=hostnames,
526 status=status,
527 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000528 return self.run('reverify_hosts', **query_args)
529
530
mbligh67647152008-11-19 00:18:14 +0000531 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000532 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000533 return self.get_hosts(id=id)[0]
534
535
MK Ryuacf35922014-10-03 14:56:49 -0700536 def get_host_attribute(self, attr, **dargs):
537 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
538 return [HostAttribute(self, a) for a in host_attrs]
539
540
Chris Masone8abb6fc2012-01-31 09:27:36 -0800541 def set_host_attribute(self, attr, val, **dargs):
542 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
543
544
mbligh67647152008-11-19 00:18:14 +0000545 def get_labels(self, **dargs):
546 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000547 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000548
549
550 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000551 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000552 return self.get_labels(id=id)[0]
553
554
555 def get_acls(self, **dargs):
556 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000557 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000558
559
560 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000561 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000562 return self.get_acls(id=id)[0]
563
564
mbligh54459c72009-01-21 19:26:44 +0000565 def get_users(self, **dargs):
566 users = self.run('get_users', **dargs)
567 return [User(self, u) for u in users]
568
569
mbligh1354c9d2008-12-22 14:56:13 +0000570 def generate_control_file(self, tests, **dargs):
571 ret = self.run('generate_control_file', tests=tests, **dargs)
572 return ControlFile(self, ret)
573
574
mbligh67647152008-11-19 00:18:14 +0000575 def get_jobs(self, summary=False, **dargs):
576 if summary:
577 jobs_data = self.run('get_jobs_summary', **dargs)
578 else:
579 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000580 jobs = []
581 for j in jobs_data:
582 job = Job(self, j)
583 # Set up some extra information defaults
584 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
585 job.platform_results = {}
586 job.platform_reasons = {}
587 jobs.append(job)
588 return jobs
mbligh67647152008-11-19 00:18:14 +0000589
590
591 def get_host_queue_entries(self, **data):
592 entries = self.run('get_host_queue_entries', **data)
mblighf9e35862009-02-26 01:03:11 +0000593 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000594
595 # Sadly, get_host_queue_entries doesn't return platforms, we have
596 # to get those back from an explicit get_hosts queury, then patch
597 # the new host objects back into the host list.
598 hostnames = [s.host.hostname for s in job_statuses if s.host]
599 host_hash = {}
600 for host in self.get_hosts(hostname__in=hostnames):
601 host_hash[host.hostname] = host
602 for status in job_statuses:
603 if status.host:
Fang Deng97dafbc2015-04-23 23:06:18 -0700604 status.host = host_hash.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000605 # filter job statuses that have either host or meta_host
606 return [status for status in job_statuses if (status.host or
607 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000608
609
MK Ryu1b2d7f92015-02-24 17:45:02 -0800610 def get_special_tasks(self, **data):
611 tasks = self.run('get_special_tasks', **data)
612 return [SpecialTask(self, t) for t in tasks]
613
614
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700615 def get_host_special_tasks(self, host_id, **data):
616 tasks = self.run('get_host_special_tasks',
617 host_id=host_id, **data)
618 return [SpecialTask(self, t) for t in tasks]
619
620
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700621 def get_host_status_task(self, host_id, end_time):
622 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700623 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700624 return SpecialTask(self, task) if task else None
625
626
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700627 def get_host_diagnosis_interval(self, host_id, end_time, success):
628 return self.run('get_host_diagnosis_interval',
629 host_id=host_id, end_time=end_time,
630 success=success)
631
632
mbligh67647152008-11-19 00:18:14 +0000633 def create_job(self, control_file, name=' ', priority='Medium',
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700634 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT, **dargs):
mbligh67647152008-11-19 00:18:14 +0000635 id = self.run('create_job', name=name, priority=priority,
636 control_file=control_file, control_type=control_type, **dargs)
637 return self.get_jobs(id=id)[0]
638
639
Simran Basi01984f52015-10-12 15:36:45 -0700640 def abort_jobs(self, jobs):
641 """Abort a list of jobs.
642
643 Already completed jobs will not be affected.
644
645 @param jobs: List of job ids to abort.
646 """
647 for job in jobs:
648 self.run('abort_host_queue_entries', job_id=job)
649
650
Kevin Cheng19521982016-09-22 12:27:23 -0700651 def get_hosts_by_attribute(self, attribute, value):
652 """
653 Get the list of hosts that share the same host attribute value.
654
655 @param attribute: String of the host attribute to check.
656 @param value: String of the value that is shared between hosts.
657
658 @returns List of hostnames that all have the same host attribute and
659 value.
660 """
661 return self.run('get_hosts_by_attribute',
662 attribute=attribute, value=value)
663
664
665 def lock_host(self, host, lock_reason, fail_if_locked=False):
666 """
667 Lock the given host with the given lock reason.
668
669 Locking a host that's already locked using the 'modify_hosts' rpc
670 will raise an exception. That's why fail_if_locked exists so the
671 caller can determine if the lock succeeded or failed. This will
672 save every caller from wrapping lock_host in a try-except.
673
674 @param host: hostname of host to lock.
675 @param lock_reason: Reason for locking host.
676 @param fail_if_locked: Return False if host is already locked.
677
678 @returns Boolean, True if lock was successful, False otherwise.
679 """
680 try:
681 self.run('modify_hosts',
682 host_filter_data={'hostname': host},
683 update_data={'locked': True,
684 'lock_reason': lock_reason})
685 except Exception:
686 return not fail_if_locked
687 return True
688
689
690 def unlock_hosts(self, locked_hosts):
691 """
692 Unlock the hosts.
693
694 Unlocking a host that's already unlocked will do nothing so we don't
695 need any special try-except clause here.
696
697 @param locked_hosts: List of hostnames of hosts to unlock.
698 """
699 self.run('modify_hosts',
700 host_filter_data={'hostname__in': locked_hosts},
701 update_data={'locked': False,
702 'lock_reason': ''})
703
704
mbligh5280e3b2008-12-22 14:39:28 +0000705class TestResults(object):
706 """
707 Container class used to hold the results of the tests for a job
708 """
709 def __init__(self):
710 self.good = []
711 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000712 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000713
714
715 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000716 if result.complete_count > result.pass_count:
717 self.fail.append(result)
718 elif result.incomplete_count > 0:
719 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000720 else:
mbligh451ede12009-02-12 21:54:03 +0000721 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000722
723
724class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000725 """
726 Generic object used to construct python objects from rpc calls
727 """
728 def __init__(self, afe, hash):
729 self.afe = afe
730 self.hash = hash
731 self.__dict__.update(hash)
732
733
734 def __str__(self):
735 return dump_object(self.__repr__(), self)
736
737
mbligh1354c9d2008-12-22 14:56:13 +0000738class ControlFile(RpcObject):
739 """
740 AFE control file object
741
742 Fields: synch_count, dependencies, control_file, is_server
743 """
744 def __repr__(self):
745 return 'CONTROL FILE: %s' % self.control_file
746
747
mbligh5280e3b2008-12-22 14:39:28 +0000748class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000749 """
750 AFE label object
751
752 Fields:
753 name, invalid, platform, kernel_config, id, only_if_needed
754 """
755 def __repr__(self):
756 return 'LABEL: %s' % self.name
757
758
759 def add_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800760 return self.afe.run('label_add_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000761
762
763 def remove_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800764 return self.afe.run('label_remove_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000765
766
mbligh5280e3b2008-12-22 14:39:28 +0000767class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000768 """
769 AFE acl object
770
771 Fields:
772 users, hosts, description, name, id
773 """
774 def __repr__(self):
775 return 'ACL: %s' % self.name
776
777
778 def add_hosts(self, hosts):
779 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
780 return self.afe.run('acl_group_add_hosts', self.id, hosts)
781
782
783 def remove_hosts(self, hosts):
784 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
785 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
786
787
mbligh54459c72009-01-21 19:26:44 +0000788 def add_users(self, users):
789 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
790 return self.afe.run('acl_group_add_users', id=self.name, users=users)
791
792
mbligh5280e3b2008-12-22 14:39:28 +0000793class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000794 """
795 AFE job object
796
797 Fields:
798 name, control_file, control_type, synch_count, reboot_before,
799 run_verify, priority, email_list, created_on, dependencies,
800 timeout, owner, reboot_after, id
801 """
802 def __repr__(self):
803 return 'JOB: %s' % self.id
804
805
mbligh5280e3b2008-12-22 14:39:28 +0000806class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000807 """
808 AFE job_status object
809
810 Fields:
811 status, complete, deleted, meta_host, host, active, execution_subdir, id
812 """
813 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800814 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000815 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700816 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000817 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000818
819
820 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000821 if self.host and self.host.hostname:
822 hostname = self.host.hostname
823 else:
824 hostname = 'None'
825 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000826
827
MK Ryu1b2d7f92015-02-24 17:45:02 -0800828class SpecialTask(RpcObject):
829 """
830 AFE special task object
831 """
832 def __init__(self, afe, hash):
833 super(SpecialTask, self).__init__(afe, hash)
834 self.host = Host(afe, self.host)
835
836
837 def __repr__(self):
838 return 'SPECIAL TASK: %s' % self.id
839
840
mbligh5280e3b2008-12-22 14:39:28 +0000841class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000842 """
843 AFE host object
844
845 Fields:
846 status, lock_time, locked_by, locked, hostname, invalid,
847 synch_id, labels, platform, protection, dirty, id
848 """
849 def __repr__(self):
850 return 'HOST OBJECT: %s' % self.hostname
851
852
853 def show(self):
854 labels = list(set(self.labels) - set([self.platform]))
855 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
856 self.locked, self.platform,
857 ', '.join(labels))
858
859
mbligh54459c72009-01-21 19:26:44 +0000860 def delete(self):
861 return self.afe.run('delete_host', id=self.id)
862
863
mbligh6463c4b2009-01-30 00:33:37 +0000864 def modify(self, **dargs):
865 return self.afe.run('modify_host', id=self.id, **dargs)
866
867
mbligh67647152008-11-19 00:18:14 +0000868 def get_acls(self):
869 return self.afe.get_acls(hosts__hostname=self.hostname)
870
871
872 def add_acl(self, acl_name):
873 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
874 return self.afe.run('acl_group_add_hosts', id=acl_name,
875 hosts=[self.hostname])
876
877
878 def remove_acl(self, acl_name):
879 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
880 return self.afe.run('acl_group_remove_hosts', id=acl_name,
881 hosts=[self.hostname])
882
883
884 def get_labels(self):
885 return self.afe.get_labels(host__hostname__in=[self.hostname])
886
887
888 def add_labels(self, labels):
889 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
890 return self.afe.run('host_add_labels', id=self.id, labels=labels)
891
892
893 def remove_labels(self, labels):
894 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
895 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000896
897
mbligh54459c72009-01-21 19:26:44 +0000898class User(RpcObject):
899 def __repr__(self):
900 return 'USER: %s' % self.login
901
902
mbligh5280e3b2008-12-22 14:39:28 +0000903class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000904 """
905 TKO test status object
906
907 Fields:
908 test_idx, hostname, testname, id
909 complete_count, incomplete_count, group_count, pass_count
910 """
911 def __repr__(self):
912 return 'TEST STATUS: %s' % self.id
913
914
MK Ryuacf35922014-10-03 14:56:49 -0700915class HostAttribute(RpcObject):
916 """
917 AFE host attribute object
918
919 Fields:
920 id, host, attribute, value
921 """
922 def __repr__(self):
923 return 'HOST ATTRIBUTE %d' % self.id