blob: 3c143abf9dc879ba0cae3b88fedc992529e4795c [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
Richard Barnette73e74b02017-08-10 15:16:49 -0700548 def repair_hosts(self, hostnames=(), status=None, label=None):
549 query_args = dict(locked=False,
550 aclgroup__users__login=self.user)
551 query_args.update(self._dict_for_host_query(hostnames=hostnames,
552 status=status,
553 label=label))
554 return self.run('repair_hosts', **query_args)
555
556
mbligh67647152008-11-19 00:18:14 +0000557 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000558 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000559 return self.get_hosts(id=id)[0]
560
561
MK Ryuacf35922014-10-03 14:56:49 -0700562 def get_host_attribute(self, attr, **dargs):
563 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
564 return [HostAttribute(self, a) for a in host_attrs]
565
566
Chris Masone8abb6fc2012-01-31 09:27:36 -0800567 def set_host_attribute(self, attr, val, **dargs):
568 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
569
570
mbligh67647152008-11-19 00:18:14 +0000571 def get_labels(self, **dargs):
572 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000573 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000574
575
576 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000577 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000578 return self.get_labels(id=id)[0]
579
580
581 def get_acls(self, **dargs):
582 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000583 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000584
585
586 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000587 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000588 return self.get_acls(id=id)[0]
589
590
mbligh54459c72009-01-21 19:26:44 +0000591 def get_users(self, **dargs):
592 users = self.run('get_users', **dargs)
593 return [User(self, u) for u in users]
594
595
mbligh1354c9d2008-12-22 14:56:13 +0000596 def generate_control_file(self, tests, **dargs):
597 ret = self.run('generate_control_file', tests=tests, **dargs)
598 return ControlFile(self, ret)
599
600
mbligh67647152008-11-19 00:18:14 +0000601 def get_jobs(self, summary=False, **dargs):
602 if summary:
603 jobs_data = self.run('get_jobs_summary', **dargs)
604 else:
605 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000606 jobs = []
607 for j in jobs_data:
608 job = Job(self, j)
609 # Set up some extra information defaults
610 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
611 job.platform_results = {}
612 job.platform_reasons = {}
613 jobs.append(job)
614 return jobs
mbligh67647152008-11-19 00:18:14 +0000615
616
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700617 def get_host_queue_entries(self, **kwargs):
618 """Find JobStatus objects matching some constraints.
mbligh99b24f42009-06-08 16:45:55 +0000619
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700620 @param **kwargs: Arguments to pass to the RPC
621 """
622 entries = self.run('get_host_queue_entries', **kwargs)
623 return self._entries_to_statuses(entries)
624
625
626 def get_host_queue_entries_by_insert_time(self, **kwargs):
627 """Like get_host_queue_entries, but using the insert index table.
628
629 @param **kwargs: Arguments to pass to the RPC
630 """
631 entries = self.run('get_host_queue_entries_by_insert_time', **kwargs)
632 return self._entries_to_statuses(entries)
633
634
635 def _entries_to_statuses(self, entries):
636 """Converts HQEs to JobStatuses
637
638 Sadly, get_host_queue_entries doesn't return platforms, we have
639 to get those back from an explicit get_hosts queury, then patch
640 the new host objects back into the host list.
641
642 :param entries: A list of HQEs from get_host_queue_entries or
643 get_host_queue_entries_by_insert_time.
644 """
645 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000646 hostnames = [s.host.hostname for s in job_statuses if s.host]
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700647 hosts = {}
mbligh99b24f42009-06-08 16:45:55 +0000648 for host in self.get_hosts(hostname__in=hostnames):
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700649 hosts[host.hostname] = host
mbligh99b24f42009-06-08 16:45:55 +0000650 for status in job_statuses:
651 if status.host:
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700652 status.host = hosts.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000653 # filter job statuses that have either host or meta_host
654 return [status for status in job_statuses if (status.host or
655 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000656
657
MK Ryu1b2d7f92015-02-24 17:45:02 -0800658 def get_special_tasks(self, **data):
659 tasks = self.run('get_special_tasks', **data)
660 return [SpecialTask(self, t) for t in tasks]
661
662
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700663 def get_host_special_tasks(self, host_id, **data):
664 tasks = self.run('get_host_special_tasks',
665 host_id=host_id, **data)
666 return [SpecialTask(self, t) for t in tasks]
667
668
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700669 def get_host_status_task(self, host_id, end_time):
670 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700671 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700672 return SpecialTask(self, task) if task else None
673
674
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700675 def get_host_diagnosis_interval(self, host_id, end_time, success):
676 return self.run('get_host_diagnosis_interval',
677 host_id=host_id, end_time=end_time,
678 success=success)
679
680
Allen Li352b86a2016-12-14 12:11:27 -0800681 def create_job(self, control_file, name=' ',
682 priority=priorities.Priority.DEFAULT,
683 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT,
684 **dargs):
mbligh67647152008-11-19 00:18:14 +0000685 id = self.run('create_job', name=name, priority=priority,
686 control_file=control_file, control_type=control_type, **dargs)
687 return self.get_jobs(id=id)[0]
688
689
Simran Basi01984f52015-10-12 15:36:45 -0700690 def abort_jobs(self, jobs):
691 """Abort a list of jobs.
692
693 Already completed jobs will not be affected.
694
695 @param jobs: List of job ids to abort.
696 """
697 for job in jobs:
698 self.run('abort_host_queue_entries', job_id=job)
699
700
Kevin Cheng19521982016-09-22 12:27:23 -0700701 def get_hosts_by_attribute(self, attribute, value):
702 """
703 Get the list of hosts that share the same host attribute value.
704
705 @param attribute: String of the host attribute to check.
706 @param value: String of the value that is shared between hosts.
707
708 @returns List of hostnames that all have the same host attribute and
709 value.
710 """
711 return self.run('get_hosts_by_attribute',
712 attribute=attribute, value=value)
713
714
715 def lock_host(self, host, lock_reason, fail_if_locked=False):
716 """
717 Lock the given host with the given lock reason.
718
719 Locking a host that's already locked using the 'modify_hosts' rpc
720 will raise an exception. That's why fail_if_locked exists so the
721 caller can determine if the lock succeeded or failed. This will
722 save every caller from wrapping lock_host in a try-except.
723
724 @param host: hostname of host to lock.
725 @param lock_reason: Reason for locking host.
726 @param fail_if_locked: Return False if host is already locked.
727
728 @returns Boolean, True if lock was successful, False otherwise.
729 """
730 try:
731 self.run('modify_hosts',
732 host_filter_data={'hostname': host},
733 update_data={'locked': True,
734 'lock_reason': lock_reason})
735 except Exception:
736 return not fail_if_locked
737 return True
738
739
740 def unlock_hosts(self, locked_hosts):
741 """
742 Unlock the hosts.
743
744 Unlocking a host that's already unlocked will do nothing so we don't
745 need any special try-except clause here.
746
747 @param locked_hosts: List of hostnames of hosts to unlock.
748 """
749 self.run('modify_hosts',
750 host_filter_data={'hostname__in': locked_hosts},
751 update_data={'locked': False,
752 'lock_reason': ''})
753
754
mbligh5280e3b2008-12-22 14:39:28 +0000755class TestResults(object):
756 """
757 Container class used to hold the results of the tests for a job
758 """
759 def __init__(self):
760 self.good = []
761 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000762 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000763
764
765 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000766 if result.complete_count > result.pass_count:
767 self.fail.append(result)
768 elif result.incomplete_count > 0:
769 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000770 else:
mbligh451ede12009-02-12 21:54:03 +0000771 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000772
773
774class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000775 """
776 Generic object used to construct python objects from rpc calls
777 """
778 def __init__(self, afe, hash):
779 self.afe = afe
780 self.hash = hash
781 self.__dict__.update(hash)
782
783
784 def __str__(self):
785 return dump_object(self.__repr__(), self)
786
787
mbligh1354c9d2008-12-22 14:56:13 +0000788class ControlFile(RpcObject):
789 """
790 AFE control file object
791
792 Fields: synch_count, dependencies, control_file, is_server
793 """
794 def __repr__(self):
795 return 'CONTROL FILE: %s' % self.control_file
796
797
mbligh5280e3b2008-12-22 14:39:28 +0000798class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000799 """
800 AFE label object
801
802 Fields:
803 name, invalid, platform, kernel_config, id, only_if_needed
804 """
805 def __repr__(self):
806 return 'LABEL: %s' % self.name
807
808
809 def add_hosts(self, hosts):
Prathmesh Prabhuef7e5b02017-11-08 11:57:47 -0800810 # We must use the label's name instead of the id because label ids are
811 # not consistent across master-shard.
812 return self.afe.run('label_add_hosts', id=self.name, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000813
814
815 def remove_hosts(self, hosts):
Prathmesh Prabhuef7e5b02017-11-08 11:57:47 -0800816 # We must use the label's name instead of the id because label ids are
817 # not consistent across master-shard.
818 return self.afe.run('label_remove_hosts', id=self.name, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000819
820
mbligh5280e3b2008-12-22 14:39:28 +0000821class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000822 """
823 AFE acl object
824
825 Fields:
826 users, hosts, description, name, id
827 """
828 def __repr__(self):
829 return 'ACL: %s' % self.name
830
831
832 def add_hosts(self, hosts):
833 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
834 return self.afe.run('acl_group_add_hosts', self.id, hosts)
835
836
837 def remove_hosts(self, hosts):
838 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
839 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
840
841
mbligh54459c72009-01-21 19:26:44 +0000842 def add_users(self, users):
843 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
844 return self.afe.run('acl_group_add_users', id=self.name, users=users)
845
846
mbligh5280e3b2008-12-22 14:39:28 +0000847class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000848 """
849 AFE job object
850
851 Fields:
852 name, control_file, control_type, synch_count, reboot_before,
853 run_verify, priority, email_list, created_on, dependencies,
854 timeout, owner, reboot_after, id
855 """
856 def __repr__(self):
857 return 'JOB: %s' % self.id
858
859
mbligh5280e3b2008-12-22 14:39:28 +0000860class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000861 """
862 AFE job_status object
863
864 Fields:
865 status, complete, deleted, meta_host, host, active, execution_subdir, id
866 """
867 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800868 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000869 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700870 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000871 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000872
873
874 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000875 if self.host and self.host.hostname:
876 hostname = self.host.hostname
877 else:
878 hostname = 'None'
879 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000880
881
MK Ryu1b2d7f92015-02-24 17:45:02 -0800882class SpecialTask(RpcObject):
883 """
884 AFE special task object
885 """
886 def __init__(self, afe, hash):
887 super(SpecialTask, self).__init__(afe, hash)
888 self.host = Host(afe, self.host)
889
890
891 def __repr__(self):
892 return 'SPECIAL TASK: %s' % self.id
893
894
mbligh5280e3b2008-12-22 14:39:28 +0000895class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000896 """
897 AFE host object
898
899 Fields:
900 status, lock_time, locked_by, locked, hostname, invalid,
Allen Lie4c08272017-02-01 16:40:53 -0800901 labels, platform, protection, dirty, id
mbligh67647152008-11-19 00:18:14 +0000902 """
903 def __repr__(self):
904 return 'HOST OBJECT: %s' % self.hostname
905
906
907 def show(self):
908 labels = list(set(self.labels) - set([self.platform]))
909 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
910 self.locked, self.platform,
911 ', '.join(labels))
912
913
mbligh54459c72009-01-21 19:26:44 +0000914 def delete(self):
915 return self.afe.run('delete_host', id=self.id)
916
917
mbligh6463c4b2009-01-30 00:33:37 +0000918 def modify(self, **dargs):
919 return self.afe.run('modify_host', id=self.id, **dargs)
920
921
mbligh67647152008-11-19 00:18:14 +0000922 def get_acls(self):
923 return self.afe.get_acls(hosts__hostname=self.hostname)
924
925
926 def add_acl(self, acl_name):
927 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
928 return self.afe.run('acl_group_add_hosts', id=acl_name,
929 hosts=[self.hostname])
930
931
932 def remove_acl(self, acl_name):
933 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
934 return self.afe.run('acl_group_remove_hosts', id=acl_name,
935 hosts=[self.hostname])
936
937
938 def get_labels(self):
939 return self.afe.get_labels(host__hostname__in=[self.hostname])
940
941
942 def add_labels(self, labels):
943 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
944 return self.afe.run('host_add_labels', id=self.id, labels=labels)
945
946
947 def remove_labels(self, labels):
948 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
949 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000950
951
mbligh54459c72009-01-21 19:26:44 +0000952class User(RpcObject):
953 def __repr__(self):
954 return 'USER: %s' % self.login
955
956
mbligh5280e3b2008-12-22 14:39:28 +0000957class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000958 """
959 TKO test status object
960
961 Fields:
962 test_idx, hostname, testname, id
963 complete_count, incomplete_count, group_count, pass_count
964 """
965 def __repr__(self):
966 return 'TEST STATUS: %s' % self.id
967
968
MK Ryuacf35922014-10-03 14:56:49 -0700969class HostAttribute(RpcObject):
970 """
971 AFE host attribute object
972
973 Fields:
974 id, host, attribute, value
975 """
976 def __repr__(self):
977 return 'HOST ATTRIBUTE %d' % self.id