blob: 452d3bfab6b7b63e281a1f99e43ac953e12250d8 [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
Allen Li327e6fd2016-11-22 13:45:41 -080023
mbligh67647152008-11-19 00:18:14 +000024from autotest_lib.frontend.afe import rpc_client_lib
Dan Shie8e0c052015-09-01 00:27:27 -070025from autotest_lib.client.common_lib import control_data
mbligh37eceaa2008-12-15 22:56:37 +000026from autotest_lib.client.common_lib import global_config
Allen Li352b86a2016-12-14 12:11:27 -080027from autotest_lib.client.common_lib import priorities
mbligh67647152008-11-19 00:18:14 +000028from autotest_lib.client.common_lib import utils
Scott Zawalski63470dd2012-09-05 00:49:43 -040029from autotest_lib.tko import db
30
Dan Shi5e2efb72017-02-07 11:40:23 -080031try:
32 from chromite.lib import metrics
33except ImportError:
34 metrics = utils.metrics_mock
Scott Zawalski63470dd2012-09-05 00:49:43 -040035
mbligh4e576612008-12-22 14:56:36 +000036try:
37 from autotest_lib.server.site_common import site_utils as server_utils
38except:
39 from autotest_lib.server import utils as server_utils
40form_ntuples_from_machines = server_utils.form_ntuples_from_machines
mbligh67647152008-11-19 00:18:14 +000041
mbligh37eceaa2008-12-15 22:56:37 +000042GLOBAL_CONFIG = global_config.global_config
43DEFAULT_SERVER = 'autotest'
44
Dan Shie8e0c052015-09-01 00:27:27 -070045
mbligh67647152008-11-19 00:18:14 +000046def dump_object(header, obj):
47 """
48 Standard way to print out the frontend objects (eg job, host, acl, label)
49 in a human-readable fashion for debugging
50 """
51 result = header + '\n'
52 for key in obj.hash:
53 if key == 'afe' or key == 'hash':
54 continue
55 result += '%20s: %s\n' % (key, obj.hash[key])
56 return result
57
58
mbligh5280e3b2008-12-22 14:39:28 +000059class RpcClient(object):
mbligh67647152008-11-19 00:18:14 +000060 """
mbligh451ede12009-02-12 21:54:03 +000061 Abstract RPC class for communicating with the autotest frontend
62 Inherited for both TKO and AFE uses.
mbligh67647152008-11-19 00:18:14 +000063
mbligh1ef218d2009-08-03 16:57:56 +000064 All the constructors go in the afe / tko class.
mbligh451ede12009-02-12 21:54:03 +000065 Manipulating methods go in the object classes themselves
mbligh67647152008-11-19 00:18:14 +000066 """
mbligh99b24f42009-06-08 16:45:55 +000067 def __init__(self, path, user, server, print_log, debug, reply_debug):
mbligh67647152008-11-19 00:18:14 +000068 """
mbligh451ede12009-02-12 21:54:03 +000069 Create a cached instance of a connection to the frontend
mbligh67647152008-11-19 00:18:14 +000070
71 user: username to connect as
mbligh451ede12009-02-12 21:54:03 +000072 server: frontend server to connect to
mbligh67647152008-11-19 00:18:14 +000073 print_log: pring a logging message to stdout on every operation
74 debug: print out all RPC traffic
75 """
Dan Shiff78f112015-06-12 13:34:02 -070076 if not user and utils.is_in_container():
77 user = GLOBAL_CONFIG.get_config_value('SSP', 'user', default=None)
mblighc31e4022008-12-11 19:32:30 +000078 if not user:
mblighdb59e3c2009-11-21 01:45:18 +000079 user = getpass.getuser()
mbligh451ede12009-02-12 21:54:03 +000080 if not server:
mbligh475f7762009-01-30 00:34:04 +000081 if 'AUTOTEST_WEB' in os.environ:
mbligh451ede12009-02-12 21:54:03 +000082 server = os.environ['AUTOTEST_WEB']
mbligh475f7762009-01-30 00:34:04 +000083 else:
mbligh451ede12009-02-12 21:54:03 +000084 server = GLOBAL_CONFIG.get_config_value('SERVER', 'hostname',
85 default=DEFAULT_SERVER)
86 self.server = server
mbligh67647152008-11-19 00:18:14 +000087 self.user = user
88 self.print_log = print_log
89 self.debug = debug
mbligh99b24f42009-06-08 16:45:55 +000090 self.reply_debug = reply_debug
Scott Zawalski347aaf42012-04-03 16:33:00 -040091 headers = {'AUTHORIZATION': self.user}
92 rpc_server = 'http://' + server + path
mbligh1354c9d2008-12-22 14:56:13 +000093 if debug:
94 print 'SERVER: %s' % rpc_server
95 print 'HEADERS: %s' % headers
mbligh67647152008-11-19 00:18:14 +000096 self.proxy = rpc_client_lib.get_proxy(rpc_server, headers=headers)
97
98
99 def run(self, call, **dargs):
100 """
101 Make a RPC call to the AFE server
102 """
103 rpc_call = getattr(self.proxy, call)
104 if self.debug:
105 print 'DEBUG: %s %s' % (call, dargs)
mbligh451ede12009-02-12 21:54:03 +0000106 try:
mbligh99b24f42009-06-08 16:45:55 +0000107 result = utils.strip_unicode(rpc_call(**dargs))
108 if self.reply_debug:
109 print result
110 return result
mbligh451ede12009-02-12 21:54:03 +0000111 except Exception:
mbligh451ede12009-02-12 21:54:03 +0000112 raise
mbligh67647152008-11-19 00:18:14 +0000113
114
115 def log(self, message):
116 if self.print_log:
117 print message
118
119
mbligh5280e3b2008-12-22 14:39:28 +0000120class TKO(RpcClient):
mbligh99b24f42009-06-08 16:45:55 +0000121 def __init__(self, user=None, server=None, print_log=True, debug=False,
122 reply_debug=False):
Scott Zawalski347aaf42012-04-03 16:33:00 -0400123 super(TKO, self).__init__(path='/new_tko/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000124 user=user,
125 server=server,
126 print_log=print_log,
127 debug=debug,
128 reply_debug=reply_debug)
Scott Zawalski63470dd2012-09-05 00:49:43 -0400129 self._db = None
130
131
Allen Li327e6fd2016-11-22 13:45:41 -0800132 @metrics.SecondsTimerDecorator(
Prathmesh Prabhua7556a92017-02-01 14:18:41 -0800133 'chromeos/autotest/tko/get_job_status_duration')
Scott Zawalski63470dd2012-09-05 00:49:43 -0400134 def get_job_test_statuses_from_db(self, job_id):
135 """Get job test statuses from the database.
136
137 Retrieve a set of fields from a job that reflect the status of each test
138 run within a job.
139 fields retrieved: status, test_name, reason, test_started_time,
140 test_finished_time, afe_job_id, job_owner, hostname.
141
142 @param job_id: The afe job id to look up.
143 @returns a TestStatus object of the resulting information.
144 """
145 if self._db is None:
Dan Shie8e0c052015-09-01 00:27:27 -0700146 self._db = db.db()
Fang Deng5c508332014-03-19 10:26:00 -0700147 fields = ['status', 'test_name', 'subdir', 'reason',
148 'test_started_time', 'test_finished_time', 'afe_job_id',
149 'job_owner', 'hostname', 'job_tag']
Scott Zawalski63470dd2012-09-05 00:49:43 -0400150 table = 'tko_test_view_2'
151 where = 'job_tag like "%s-%%"' % job_id
152 test_status = []
153 # Run commit before we query to ensure that we are pulling the latest
154 # results.
155 self._db.commit()
156 for entry in self._db.select(','.join(fields), table, (where, None)):
157 status_dict = {}
158 for key,value in zip(fields, entry):
159 # All callers expect values to be a str object.
160 status_dict[key] = str(value)
161 # id is used by TestStatus to uniquely identify each Test Status
162 # obj.
163 status_dict['id'] = [status_dict['reason'], status_dict['hostname'],
164 status_dict['test_name']]
165 test_status.append(status_dict)
166
167 return [TestStatus(self, e) for e in test_status]
mblighc31e4022008-12-11 19:32:30 +0000168
169
170 def get_status_counts(self, job, **data):
171 entries = self.run('get_status_counts',
mbligh1ef218d2009-08-03 16:57:56 +0000172 group_by=['hostname', 'test_name', 'reason'],
mblighc31e4022008-12-11 19:32:30 +0000173 job_tag__startswith='%s-' % job, **data)
mbligh5280e3b2008-12-22 14:39:28 +0000174 return [TestStatus(self, e) for e in entries['groups']]
mblighc31e4022008-12-11 19:32:30 +0000175
176
Richard Barnette260cbd02016-10-06 12:23:28 -0700177class _StableVersionMap(object):
178 """
179 A mapping from board names to strings naming software versions.
180
181 The mapping is meant to allow finding a nominally "stable" version
182 of software associated with a given board. The mapping identifies
183 specific versions of software that should be installed during
184 operations such as repair.
185
186 Conceptually, there are multiple version maps, each handling
187 different types of image. For instance, a single board may have
188 both a stable OS image (e.g. for CrOS), and a separate stable
189 firmware image.
190
191 Each different type of image requires a certain amount of special
192 handling, implemented by a subclass of `StableVersionMap`. The
193 subclasses take care of pre-processing of arguments, delegating
194 actual RPC calls to this superclass.
195
196 @property _afe AFE object through which to make the actual RPC
197 calls.
198 @property _android Value of the `android` parameter to be passed
199 when calling the `get_stable_version` RPC.
200 """
201
202 # DEFAULT_BOARD - The stable_version RPC API recognizes this special
203 # name as a mapping to use when no specific mapping for a board is
204 # present. This default mapping is only allowed for CrOS image
205 # types; other image type subclasses exclude it.
206 #
207 # TODO(jrbarnette): This value is copied from
208 # site_utils.stable_version_utils, because if we import that
209 # module here, it breaks unit tests. Something about the Django
210 # setup...
211 DEFAULT_BOARD = 'DEFAULT'
212
213
214 def __init__(self, afe, android):
215 self._afe = afe
216 self._android = android
217
218
219 def get_all_versions(self):
220 """
221 Get all mappings in the stable versions table.
222
223 Extracts the full content of the `stable_version` table
224 in the AFE database, and returns it as a dictionary
225 mapping board names to version strings.
226
227 @return A dictionary mapping board names to version strings.
228 """
229 return self._afe.run('get_all_stable_versions')
230
231
232 def get_version(self, board):
233 """
234 Get the mapping of one board in the stable versions table.
235
236 Look up and return the version mapped to the given board in the
237 `stable_versions` table in the AFE database.
238
239 @param board The board to be looked up.
240
241 @return The version mapped for the given board.
242 """
243 return self._afe.run('get_stable_version',
244 board=board, android=self._android)
245
246
247 def set_version(self, board, version):
248 """
249 Change the mapping of one board in the stable versions table.
250
251 Set the mapping in the `stable_versions` table in the AFE
252 database for the given board to the given version.
253
254 @param board The board to be updated.
255 @param version The new version to be assigned to the board.
256 """
257 self._afe.run('set_stable_version',
258 version=version, board=board)
259
260
261 def delete_version(self, board):
262 """
263 Remove the mapping of one board in the stable versions table.
264
265 Remove the mapping in the `stable_versions` table in the AFE
266 database for the given board.
267
268 @param board The board to be updated.
269 """
270 self._afe.run('delete_stable_version', board=board)
271
272
273class _OSVersionMap(_StableVersionMap):
274 """
275 Abstract stable version mapping for full OS images of various types.
276 """
277
278 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700279 # TODO(jrbarnette): We exclude non-OS (i.e. firmware) version
280 # mappings, but the returned dict doesn't distinguish CrOS
281 # boards from Android boards; both will be present, and the
282 # subclass can't distinguish them.
Richard Barnette260cbd02016-10-06 12:23:28 -0700283 #
284 # Ultimately, the right fix is to move knowledge of image type
285 # over to the RPC server side.
286 #
287 versions = super(_OSVersionMap, self).get_all_versions()
288 for board in versions.keys():
289 if '/' in board:
290 del versions[board]
291 return versions
292
293
294class _CrosVersionMap(_OSVersionMap):
295 """
296 Stable version mapping for Chrome OS release images.
297
298 This class manages a mapping of Chrome OS board names to known-good
299 release (or canary) images. The images selected can be installed on
300 DUTs during repair tasks, as a way of getting a DUT into a known
301 working state.
302 """
303
304 def __init__(self, afe):
305 super(_CrosVersionMap, self).__init__(afe, False)
306
Richard Barnette728e36f2016-11-03 16:04:29 -0700307 @staticmethod
308 def format_image_name(board, version):
309 """
310 Return an image name for a given `board` and `version`.
311
312 This formats `board` and `version` into a string identifying an
313 image file. The string represents part of a URL for access to
314 the image.
315
316 The returned image name is typically of a form like
317 "falco-release/R55-8872.44.0".
318 """
319 build_pattern = GLOBAL_CONFIG.get_config_value(
320 'CROS', 'stable_build_pattern')
321 return build_pattern % (board, version)
Richard Barnette260cbd02016-10-06 12:23:28 -0700322
Richard Barnette383ef9c2016-12-13 11:56:49 -0800323 def get_image_name(self, board):
324 """
325 Return the full image name of the stable version for `board`.
326
327 This finds the stable version for `board`, and returns a string
Richard Barnette728e36f2016-11-03 16:04:29 -0700328 identifying the associated image as for `format_image_name()`,
329 above.
Richard Barnette383ef9c2016-12-13 11:56:49 -0800330
331 @return A string identifying the image file for the stable
332 image for `board`.
333 """
Richard Barnette728e36f2016-11-03 16:04:29 -0700334 return self.format_image_name(board, self.get_version(board))
Richard Barnette260cbd02016-10-06 12:23:28 -0700335
336
337class _AndroidVersionMap(_OSVersionMap):
338 """
339 Stable version mapping for Android release images.
340
341 This class manages a mapping of Android/Brillo board names to
342 known-good images.
343 """
344
345 def __init__(self, afe):
346 super(_AndroidVersionMap, self).__init__(afe, True)
347
348
349 def get_all_versions(self):
350 versions = super(_AndroidVersionMap, self).get_all_versions()
351 del versions[self.DEFAULT_BOARD]
352 return versions
353
354
Richard Barnettee50453e2016-10-10 16:43:44 -0700355class _SuffixHackVersionMap(_StableVersionMap):
Richard Barnette260cbd02016-10-06 12:23:28 -0700356 """
Richard Barnettee50453e2016-10-10 16:43:44 -0700357 Abstract super class for mappings using a pseudo-board name.
Richard Barnette260cbd02016-10-06 12:23:28 -0700358
Richard Barnettee50453e2016-10-10 16:43:44 -0700359 For non-OS image type mappings, we look them up in the
360 `stable_versions` table by constructing a "pseudo-board" from the
361 real board name plus a suffix string that identifies the image type.
362 So, for instance the name "lulu/firmware" is used to look up the
363 FAFT firmware version for lulu boards.
Richard Barnette260cbd02016-10-06 12:23:28 -0700364 """
365
Richard Barnettee50453e2016-10-10 16:43:44 -0700366 # _SUFFIX - The suffix used in constructing the "pseudo-board"
367 # lookup key. Each subclass must define this value for itself.
Richard Barnette260cbd02016-10-06 12:23:28 -0700368 #
Richard Barnettee50453e2016-10-10 16:43:44 -0700369 _SUFFIX = None
Richard Barnette260cbd02016-10-06 12:23:28 -0700370
371 def __init__(self, afe):
Richard Barnettee50453e2016-10-10 16:43:44 -0700372 super(_SuffixHackVersionMap, self).__init__(afe, False)
Richard Barnette260cbd02016-10-06 12:23:28 -0700373
374
375 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700376 # Get all the mappings from the AFE, extract just the mappings
377 # with our suffix, and replace the pseudo-board name keys with
Richard Barnette260cbd02016-10-06 12:23:28 -0700378 # the real board names.
379 #
380 all_versions = super(
Richard Barnettee50453e2016-10-10 16:43:44 -0700381 _SuffixHackVersionMap, self).get_all_versions()
Richard Barnette260cbd02016-10-06 12:23:28 -0700382 return {
383 board[0 : -len(self._SUFFIX)]: all_versions[board]
384 for board in all_versions.keys()
385 if board.endswith(self._SUFFIX)
386 }
387
388
389 def get_version(self, board):
Richard Barnettee50453e2016-10-10 16:43:44 -0700390 board += self._SUFFIX
391 return super(_SuffixHackVersionMap, self).get_version(board)
392
393
394 def set_version(self, board, version):
395 board += self._SUFFIX
396 super(_SuffixHackVersionMap, self).set_version(board, version)
397
398
399 def delete_version(self, board):
400 board += self._SUFFIX
401 super(_SuffixHackVersionMap, self).delete_version(board)
402
403
404class _FAFTVersionMap(_SuffixHackVersionMap):
405 """
406 Stable version mapping for firmware versions used in FAFT repair.
407
408 When DUTs used for FAFT fail repair, stable firmware may need to be
409 flashed directly from original tarballs. The FAFT firmware version
410 mapping finds the appropriate tarball for a given board.
411 """
412
413 _SUFFIX = '/firmware'
414
415 def get_version(self, board):
416 # If there's no mapping for `board`, the lookup will return the
417 # default CrOS version mapping. To eliminate that case, we
418 # require a '/' character in the version, since CrOS versions
419 # won't match that.
Richard Barnette260cbd02016-10-06 12:23:28 -0700420 #
421 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
422 # the right fix is to move handling to the RPC server side.
423 #
Richard Barnette260cbd02016-10-06 12:23:28 -0700424 version = super(_FAFTVersionMap, self).get_version(board)
425 return version if '/' in version else None
426
427
Richard Barnettee50453e2016-10-10 16:43:44 -0700428class _FirmwareVersionMap(_SuffixHackVersionMap):
429 """
430 Stable version mapping for firmware supplied in Chrome OS images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700431
Richard Barnettee50453e2016-10-10 16:43:44 -0700432 A Chrome OS image bundles a version of the firmware that the
433 device should update to when the OS version is installed during
434 AU.
Richard Barnette260cbd02016-10-06 12:23:28 -0700435
Richard Barnettee50453e2016-10-10 16:43:44 -0700436 Test images suppress the firmware update during AU. Instead, during
437 repair and verify we check installed firmware on a DUT, compare it
438 against the stable version mapping for the board, and update when
439 the DUT is out-of-date.
440 """
441
442 _SUFFIX = '/rwfw'
443
444 def get_version(self, board):
445 # If there's no mapping for `board`, the lookup will return the
446 # default CrOS version mapping. To eliminate that case, we
447 # require the version start with "Google_", since CrOS versions
448 # won't match that.
449 #
450 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
451 # the right fix is to move handling to the RPC server side.
452 #
453 version = super(_FirmwareVersionMap, self).get_version(board)
454 return version if version.startswith('Google_') else None
Richard Barnette260cbd02016-10-06 12:23:28 -0700455
456
mbligh5280e3b2008-12-22 14:39:28 +0000457class AFE(RpcClient):
mbligh1ef218d2009-08-03 16:57:56 +0000458
Richard Barnette260cbd02016-10-06 12:23:28 -0700459 # Known image types for stable version mapping objects.
460 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
461 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
Richard Barnettee50453e2016-10-10 16:43:44 -0700462 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700463 # ANDROID_IMAGE_TYPE - Mappings for Android images.
464 #
465 CROS_IMAGE_TYPE = 'cros'
466 FAFT_IMAGE_TYPE = 'faft'
Richard Barnettee50453e2016-10-10 16:43:44 -0700467 FIRMWARE_IMAGE_TYPE = 'firmware'
Richard Barnette260cbd02016-10-06 12:23:28 -0700468 ANDROID_IMAGE_TYPE = 'android'
469
470 _IMAGE_MAPPING_CLASSES = {
471 CROS_IMAGE_TYPE: _CrosVersionMap,
472 FAFT_IMAGE_TYPE: _FAFTVersionMap,
Richard Barnettee50453e2016-10-10 16:43:44 -0700473 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
Richard Barnette260cbd02016-10-06 12:23:28 -0700474 ANDROID_IMAGE_TYPE: _AndroidVersionMap
475 }
476
477
Prathmesh Prabhu6d5ba592017-01-05 13:56:04 -0800478 def __init__(self, user=None, server=None, print_log=True, debug=False,
479 reply_debug=False, job=None):
480 self.job = job
481 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
482 user=user,
483 server=server,
484 print_log=print_log,
485 debug=debug,
486 reply_debug=reply_debug)
487
488
Richard Barnette260cbd02016-10-06 12:23:28 -0700489 def get_stable_version_map(self, image_type):
490 """
491 Return a stable version mapping for the given image type.
492
493 @return An object mapping board names to version strings for
494 software of the given image type.
495 """
496 return self._IMAGE_MAPPING_CLASSES[image_type](self)
497
498
mbligh67647152008-11-19 00:18:14 +0000499 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000500 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000501 statuses = self.run('get_static_data')['host_statuses']
502 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000503 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000504 if live == False:
505 return dead_statuses
506 else:
507 return statuses
508
509
mbligh71094012009-12-19 05:35:21 +0000510 @staticmethod
511 def _dict_for_host_query(hostnames=(), status=None, label=None):
512 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000513 if hostnames:
514 query_args['hostname__in'] = hostnames
515 if status:
516 query_args['status'] = status
517 if label:
518 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000519 return query_args
520
521
522 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
523 query_args = dict(dargs)
524 query_args.update(self._dict_for_host_query(hostnames=hostnames,
525 status=status,
526 label=label))
527 hosts = self.run('get_hosts', **query_args)
528 return [Host(self, h) for h in hosts]
529
530
531 def get_hostnames(self, status=None, label=None, **dargs):
532 """Like get_hosts() but returns hostnames instead of Host objects."""
533 # This implementation can be replaced with a more efficient one
534 # that does not query for entire host objects in the future.
535 return [host_obj.hostname for host_obj in
536 self.get_hosts(status=status, label=label, **dargs)]
537
538
539 def reverify_hosts(self, hostnames=(), status=None, label=None):
540 query_args = dict(locked=False,
541 aclgroup__users__login=self.user)
542 query_args.update(self._dict_for_host_query(hostnames=hostnames,
543 status=status,
544 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000545 return self.run('reverify_hosts', **query_args)
546
547
mbligh67647152008-11-19 00:18:14 +0000548 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000549 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000550 return self.get_hosts(id=id)[0]
551
552
MK Ryuacf35922014-10-03 14:56:49 -0700553 def get_host_attribute(self, attr, **dargs):
554 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
555 return [HostAttribute(self, a) for a in host_attrs]
556
557
Chris Masone8abb6fc2012-01-31 09:27:36 -0800558 def set_host_attribute(self, attr, val, **dargs):
559 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
560
561
mbligh67647152008-11-19 00:18:14 +0000562 def get_labels(self, **dargs):
563 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000564 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000565
566
567 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000568 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000569 return self.get_labels(id=id)[0]
570
571
572 def get_acls(self, **dargs):
573 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000574 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000575
576
577 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000578 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000579 return self.get_acls(id=id)[0]
580
581
mbligh54459c72009-01-21 19:26:44 +0000582 def get_users(self, **dargs):
583 users = self.run('get_users', **dargs)
584 return [User(self, u) for u in users]
585
586
mbligh1354c9d2008-12-22 14:56:13 +0000587 def generate_control_file(self, tests, **dargs):
588 ret = self.run('generate_control_file', tests=tests, **dargs)
589 return ControlFile(self, ret)
590
591
mbligh67647152008-11-19 00:18:14 +0000592 def get_jobs(self, summary=False, **dargs):
593 if summary:
594 jobs_data = self.run('get_jobs_summary', **dargs)
595 else:
596 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000597 jobs = []
598 for j in jobs_data:
599 job = Job(self, j)
600 # Set up some extra information defaults
601 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
602 job.platform_results = {}
603 job.platform_reasons = {}
604 jobs.append(job)
605 return jobs
mbligh67647152008-11-19 00:18:14 +0000606
607
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700608 def get_host_queue_entries(self, **kwargs):
609 """Find JobStatus objects matching some constraints.
mbligh99b24f42009-06-08 16:45:55 +0000610
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700611 @param **kwargs: Arguments to pass to the RPC
612 """
613 entries = self.run('get_host_queue_entries', **kwargs)
614 return self._entries_to_statuses(entries)
615
616
617 def get_host_queue_entries_by_insert_time(self, **kwargs):
618 """Like get_host_queue_entries, but using the insert index table.
619
620 @param **kwargs: Arguments to pass to the RPC
621 """
622 entries = self.run('get_host_queue_entries_by_insert_time', **kwargs)
623 return self._entries_to_statuses(entries)
624
625
626 def _entries_to_statuses(self, entries):
627 """Converts HQEs to JobStatuses
628
629 Sadly, get_host_queue_entries doesn't return platforms, we have
630 to get those back from an explicit get_hosts queury, then patch
631 the new host objects back into the host list.
632
633 :param entries: A list of HQEs from get_host_queue_entries or
634 get_host_queue_entries_by_insert_time.
635 """
636 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000637 hostnames = [s.host.hostname for s in job_statuses if s.host]
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700638 hosts = {}
mbligh99b24f42009-06-08 16:45:55 +0000639 for host in self.get_hosts(hostname__in=hostnames):
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700640 hosts[host.hostname] = host
mbligh99b24f42009-06-08 16:45:55 +0000641 for status in job_statuses:
642 if status.host:
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700643 status.host = hosts.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000644 # filter job statuses that have either host or meta_host
645 return [status for status in job_statuses if (status.host or
646 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000647
648
MK Ryu1b2d7f92015-02-24 17:45:02 -0800649 def get_special_tasks(self, **data):
650 tasks = self.run('get_special_tasks', **data)
651 return [SpecialTask(self, t) for t in tasks]
652
653
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700654 def get_host_special_tasks(self, host_id, **data):
655 tasks = self.run('get_host_special_tasks',
656 host_id=host_id, **data)
657 return [SpecialTask(self, t) for t in tasks]
658
659
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700660 def get_host_status_task(self, host_id, end_time):
661 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700662 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700663 return SpecialTask(self, task) if task else None
664
665
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700666 def get_host_diagnosis_interval(self, host_id, end_time, success):
667 return self.run('get_host_diagnosis_interval',
668 host_id=host_id, end_time=end_time,
669 success=success)
670
671
Allen Li352b86a2016-12-14 12:11:27 -0800672 def create_job(self, control_file, name=' ',
673 priority=priorities.Priority.DEFAULT,
674 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT,
675 **dargs):
mbligh67647152008-11-19 00:18:14 +0000676 id = self.run('create_job', name=name, priority=priority,
677 control_file=control_file, control_type=control_type, **dargs)
678 return self.get_jobs(id=id)[0]
679
680
Simran Basi01984f52015-10-12 15:36:45 -0700681 def abort_jobs(self, jobs):
682 """Abort a list of jobs.
683
684 Already completed jobs will not be affected.
685
686 @param jobs: List of job ids to abort.
687 """
688 for job in jobs:
689 self.run('abort_host_queue_entries', job_id=job)
690
691
Kevin Cheng19521982016-09-22 12:27:23 -0700692 def get_hosts_by_attribute(self, attribute, value):
693 """
694 Get the list of hosts that share the same host attribute value.
695
696 @param attribute: String of the host attribute to check.
697 @param value: String of the value that is shared between hosts.
698
699 @returns List of hostnames that all have the same host attribute and
700 value.
701 """
702 return self.run('get_hosts_by_attribute',
703 attribute=attribute, value=value)
704
705
706 def lock_host(self, host, lock_reason, fail_if_locked=False):
707 """
708 Lock the given host with the given lock reason.
709
710 Locking a host that's already locked using the 'modify_hosts' rpc
711 will raise an exception. That's why fail_if_locked exists so the
712 caller can determine if the lock succeeded or failed. This will
713 save every caller from wrapping lock_host in a try-except.
714
715 @param host: hostname of host to lock.
716 @param lock_reason: Reason for locking host.
717 @param fail_if_locked: Return False if host is already locked.
718
719 @returns Boolean, True if lock was successful, False otherwise.
720 """
721 try:
722 self.run('modify_hosts',
723 host_filter_data={'hostname': host},
724 update_data={'locked': True,
725 'lock_reason': lock_reason})
726 except Exception:
727 return not fail_if_locked
728 return True
729
730
731 def unlock_hosts(self, locked_hosts):
732 """
733 Unlock the hosts.
734
735 Unlocking a host that's already unlocked will do nothing so we don't
736 need any special try-except clause here.
737
738 @param locked_hosts: List of hostnames of hosts to unlock.
739 """
740 self.run('modify_hosts',
741 host_filter_data={'hostname__in': locked_hosts},
742 update_data={'locked': False,
743 'lock_reason': ''})
744
745
mbligh5280e3b2008-12-22 14:39:28 +0000746class TestResults(object):
747 """
748 Container class used to hold the results of the tests for a job
749 """
750 def __init__(self):
751 self.good = []
752 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000753 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000754
755
756 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000757 if result.complete_count > result.pass_count:
758 self.fail.append(result)
759 elif result.incomplete_count > 0:
760 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000761 else:
mbligh451ede12009-02-12 21:54:03 +0000762 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000763
764
765class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000766 """
767 Generic object used to construct python objects from rpc calls
768 """
769 def __init__(self, afe, hash):
770 self.afe = afe
771 self.hash = hash
772 self.__dict__.update(hash)
773
774
775 def __str__(self):
776 return dump_object(self.__repr__(), self)
777
778
mbligh1354c9d2008-12-22 14:56:13 +0000779class ControlFile(RpcObject):
780 """
781 AFE control file object
782
783 Fields: synch_count, dependencies, control_file, is_server
784 """
785 def __repr__(self):
786 return 'CONTROL FILE: %s' % self.control_file
787
788
mbligh5280e3b2008-12-22 14:39:28 +0000789class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000790 """
791 AFE label object
792
793 Fields:
794 name, invalid, platform, kernel_config, id, only_if_needed
795 """
796 def __repr__(self):
797 return 'LABEL: %s' % self.name
798
799
800 def add_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800801 return self.afe.run('label_add_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000802
803
804 def remove_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800805 return self.afe.run('label_remove_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000806
807
mbligh5280e3b2008-12-22 14:39:28 +0000808class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000809 """
810 AFE acl object
811
812 Fields:
813 users, hosts, description, name, id
814 """
815 def __repr__(self):
816 return 'ACL: %s' % self.name
817
818
819 def add_hosts(self, hosts):
820 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
821 return self.afe.run('acl_group_add_hosts', self.id, hosts)
822
823
824 def remove_hosts(self, hosts):
825 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
826 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
827
828
mbligh54459c72009-01-21 19:26:44 +0000829 def add_users(self, users):
830 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
831 return self.afe.run('acl_group_add_users', id=self.name, users=users)
832
833
mbligh5280e3b2008-12-22 14:39:28 +0000834class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000835 """
836 AFE job object
837
838 Fields:
839 name, control_file, control_type, synch_count, reboot_before,
840 run_verify, priority, email_list, created_on, dependencies,
841 timeout, owner, reboot_after, id
842 """
843 def __repr__(self):
844 return 'JOB: %s' % self.id
845
846
mbligh5280e3b2008-12-22 14:39:28 +0000847class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000848 """
849 AFE job_status object
850
851 Fields:
852 status, complete, deleted, meta_host, host, active, execution_subdir, id
853 """
854 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800855 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000856 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700857 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000858 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000859
860
861 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000862 if self.host and self.host.hostname:
863 hostname = self.host.hostname
864 else:
865 hostname = 'None'
866 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000867
868
MK Ryu1b2d7f92015-02-24 17:45:02 -0800869class SpecialTask(RpcObject):
870 """
871 AFE special task object
872 """
873 def __init__(self, afe, hash):
874 super(SpecialTask, self).__init__(afe, hash)
875 self.host = Host(afe, self.host)
876
877
878 def __repr__(self):
879 return 'SPECIAL TASK: %s' % self.id
880
881
mbligh5280e3b2008-12-22 14:39:28 +0000882class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000883 """
884 AFE host object
885
886 Fields:
887 status, lock_time, locked_by, locked, hostname, invalid,
Allen Lie4c08272017-02-01 16:40:53 -0800888 labels, platform, protection, dirty, id
mbligh67647152008-11-19 00:18:14 +0000889 """
890 def __repr__(self):
891 return 'HOST OBJECT: %s' % self.hostname
892
893
894 def show(self):
895 labels = list(set(self.labels) - set([self.platform]))
896 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
897 self.locked, self.platform,
898 ', '.join(labels))
899
900
mbligh54459c72009-01-21 19:26:44 +0000901 def delete(self):
902 return self.afe.run('delete_host', id=self.id)
903
904
mbligh6463c4b2009-01-30 00:33:37 +0000905 def modify(self, **dargs):
906 return self.afe.run('modify_host', id=self.id, **dargs)
907
908
mbligh67647152008-11-19 00:18:14 +0000909 def get_acls(self):
910 return self.afe.get_acls(hosts__hostname=self.hostname)
911
912
913 def add_acl(self, acl_name):
914 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
915 return self.afe.run('acl_group_add_hosts', id=acl_name,
916 hosts=[self.hostname])
917
918
919 def remove_acl(self, acl_name):
920 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
921 return self.afe.run('acl_group_remove_hosts', id=acl_name,
922 hosts=[self.hostname])
923
924
925 def get_labels(self):
926 return self.afe.get_labels(host__hostname__in=[self.hostname])
927
928
929 def add_labels(self, labels):
930 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
931 return self.afe.run('host_add_labels', id=self.id, labels=labels)
932
933
934 def remove_labels(self, labels):
935 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
936 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000937
938
mbligh54459c72009-01-21 19:26:44 +0000939class User(RpcObject):
940 def __repr__(self):
941 return 'USER: %s' % self.login
942
943
mbligh5280e3b2008-12-22 14:39:28 +0000944class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000945 """
946 TKO test status object
947
948 Fields:
949 test_idx, hostname, testname, id
950 complete_count, incomplete_count, group_count, pass_count
951 """
952 def __repr__(self):
953 return 'TEST STATUS: %s' % self.id
954
955
MK Ryuacf35922014-10-03 14:56:49 -0700956class HostAttribute(RpcObject):
957 """
958 AFE host attribute object
959
960 Fields:
961 id, host, attribute, value
962 """
963 def __repr__(self):
964 return 'HOST ATTRIBUTE %d' % self.id