blob: bfcba8f5cd44758170eb0c7a0db32d56798bd083 [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 Lif74957d2017-11-20 17:46:48 -080027from autotest_lib.client.common_lib import host_states
Allen Li352b86a2016-12-14 12:11:27 -080028from autotest_lib.client.common_lib import priorities
mbligh67647152008-11-19 00:18:14 +000029from autotest_lib.client.common_lib import utils
Scott Zawalski63470dd2012-09-05 00:49:43 -040030from autotest_lib.tko import db
31
Dan Shi5e2efb72017-02-07 11:40:23 -080032try:
33 from chromite.lib import metrics
34except ImportError:
35 metrics = utils.metrics_mock
Scott Zawalski63470dd2012-09-05 00:49:43 -040036
mbligh4e576612008-12-22 14:56:36 +000037try:
38 from autotest_lib.server.site_common import site_utils as server_utils
39except:
40 from autotest_lib.server import utils as server_utils
41form_ntuples_from_machines = server_utils.form_ntuples_from_machines
mbligh67647152008-11-19 00:18:14 +000042
mbligh37eceaa2008-12-15 22:56:37 +000043GLOBAL_CONFIG = global_config.global_config
44DEFAULT_SERVER = 'autotest'
45
Dan Shie8e0c052015-09-01 00:27:27 -070046
mbligh67647152008-11-19 00:18:14 +000047def dump_object(header, obj):
48 """
49 Standard way to print out the frontend objects (eg job, host, acl, label)
50 in a human-readable fashion for debugging
51 """
52 result = header + '\n'
53 for key in obj.hash:
54 if key == 'afe' or key == 'hash':
55 continue
56 result += '%20s: %s\n' % (key, obj.hash[key])
57 return result
58
59
mbligh5280e3b2008-12-22 14:39:28 +000060class RpcClient(object):
mbligh67647152008-11-19 00:18:14 +000061 """
mbligh451ede12009-02-12 21:54:03 +000062 Abstract RPC class for communicating with the autotest frontend
63 Inherited for both TKO and AFE uses.
mbligh67647152008-11-19 00:18:14 +000064
mbligh1ef218d2009-08-03 16:57:56 +000065 All the constructors go in the afe / tko class.
mbligh451ede12009-02-12 21:54:03 +000066 Manipulating methods go in the object classes themselves
mbligh67647152008-11-19 00:18:14 +000067 """
mbligh99b24f42009-06-08 16:45:55 +000068 def __init__(self, path, user, server, print_log, debug, reply_debug):
mbligh67647152008-11-19 00:18:14 +000069 """
mbligh451ede12009-02-12 21:54:03 +000070 Create a cached instance of a connection to the frontend
mbligh67647152008-11-19 00:18:14 +000071
72 user: username to connect as
mbligh451ede12009-02-12 21:54:03 +000073 server: frontend server to connect to
mbligh67647152008-11-19 00:18:14 +000074 print_log: pring a logging message to stdout on every operation
75 debug: print out all RPC traffic
76 """
Dan Shiff78f112015-06-12 13:34:02 -070077 if not user and utils.is_in_container():
78 user = GLOBAL_CONFIG.get_config_value('SSP', 'user', default=None)
mblighc31e4022008-12-11 19:32:30 +000079 if not user:
mblighdb59e3c2009-11-21 01:45:18 +000080 user = getpass.getuser()
mbligh451ede12009-02-12 21:54:03 +000081 if not server:
mbligh475f7762009-01-30 00:34:04 +000082 if 'AUTOTEST_WEB' in os.environ:
mbligh451ede12009-02-12 21:54:03 +000083 server = os.environ['AUTOTEST_WEB']
mbligh475f7762009-01-30 00:34:04 +000084 else:
mbligh451ede12009-02-12 21:54:03 +000085 server = GLOBAL_CONFIG.get_config_value('SERVER', 'hostname',
86 default=DEFAULT_SERVER)
87 self.server = server
mbligh67647152008-11-19 00:18:14 +000088 self.user = user
89 self.print_log = print_log
90 self.debug = debug
mbligh99b24f42009-06-08 16:45:55 +000091 self.reply_debug = reply_debug
Scott Zawalski347aaf42012-04-03 16:33:00 -040092 headers = {'AUTHORIZATION': self.user}
93 rpc_server = 'http://' + server + path
mbligh1354c9d2008-12-22 14:56:13 +000094 if debug:
95 print 'SERVER: %s' % rpc_server
96 print 'HEADERS: %s' % headers
mbligh67647152008-11-19 00:18:14 +000097 self.proxy = rpc_client_lib.get_proxy(rpc_server, headers=headers)
98
99
100 def run(self, call, **dargs):
101 """
102 Make a RPC call to the AFE server
103 """
104 rpc_call = getattr(self.proxy, call)
105 if self.debug:
106 print 'DEBUG: %s %s' % (call, dargs)
mbligh451ede12009-02-12 21:54:03 +0000107 try:
mbligh99b24f42009-06-08 16:45:55 +0000108 result = utils.strip_unicode(rpc_call(**dargs))
109 if self.reply_debug:
110 print result
111 return result
mbligh451ede12009-02-12 21:54:03 +0000112 except Exception:
mbligh451ede12009-02-12 21:54:03 +0000113 raise
mbligh67647152008-11-19 00:18:14 +0000114
115
116 def log(self, message):
117 if self.print_log:
118 print message
119
120
mbligh5280e3b2008-12-22 14:39:28 +0000121class TKO(RpcClient):
mbligh99b24f42009-06-08 16:45:55 +0000122 def __init__(self, user=None, server=None, print_log=True, debug=False,
123 reply_debug=False):
Scott Zawalski347aaf42012-04-03 16:33:00 -0400124 super(TKO, self).__init__(path='/new_tko/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000125 user=user,
126 server=server,
127 print_log=print_log,
128 debug=debug,
129 reply_debug=reply_debug)
Scott Zawalski63470dd2012-09-05 00:49:43 -0400130 self._db = None
131
132
Allen Li327e6fd2016-11-22 13:45:41 -0800133 @metrics.SecondsTimerDecorator(
Prathmesh Prabhua7556a92017-02-01 14:18:41 -0800134 'chromeos/autotest/tko/get_job_status_duration')
Scott Zawalski63470dd2012-09-05 00:49:43 -0400135 def get_job_test_statuses_from_db(self, job_id):
136 """Get job test statuses from the database.
137
138 Retrieve a set of fields from a job that reflect the status of each test
139 run within a job.
140 fields retrieved: status, test_name, reason, test_started_time,
141 test_finished_time, afe_job_id, job_owner, hostname.
142
143 @param job_id: The afe job id to look up.
144 @returns a TestStatus object of the resulting information.
145 """
146 if self._db is None:
Dan Shie8e0c052015-09-01 00:27:27 -0700147 self._db = db.db()
Fang Deng5c508332014-03-19 10:26:00 -0700148 fields = ['status', 'test_name', 'subdir', 'reason',
149 'test_started_time', 'test_finished_time', 'afe_job_id',
150 'job_owner', 'hostname', 'job_tag']
Scott Zawalski63470dd2012-09-05 00:49:43 -0400151 table = 'tko_test_view_2'
152 where = 'job_tag like "%s-%%"' % job_id
153 test_status = []
154 # Run commit before we query to ensure that we are pulling the latest
155 # results.
156 self._db.commit()
157 for entry in self._db.select(','.join(fields), table, (where, None)):
158 status_dict = {}
159 for key,value in zip(fields, entry):
160 # All callers expect values to be a str object.
161 status_dict[key] = str(value)
162 # id is used by TestStatus to uniquely identify each Test Status
163 # obj.
164 status_dict['id'] = [status_dict['reason'], status_dict['hostname'],
165 status_dict['test_name']]
166 test_status.append(status_dict)
167
168 return [TestStatus(self, e) for e in test_status]
mblighc31e4022008-12-11 19:32:30 +0000169
170
171 def get_status_counts(self, job, **data):
172 entries = self.run('get_status_counts',
mbligh1ef218d2009-08-03 16:57:56 +0000173 group_by=['hostname', 'test_name', 'reason'],
mblighc31e4022008-12-11 19:32:30 +0000174 job_tag__startswith='%s-' % job, **data)
mbligh5280e3b2008-12-22 14:39:28 +0000175 return [TestStatus(self, e) for e in entries['groups']]
mblighc31e4022008-12-11 19:32:30 +0000176
177
Richard Barnette260cbd02016-10-06 12:23:28 -0700178class _StableVersionMap(object):
179 """
180 A mapping from board names to strings naming software versions.
181
182 The mapping is meant to allow finding a nominally "stable" version
183 of software associated with a given board. The mapping identifies
184 specific versions of software that should be installed during
185 operations such as repair.
186
187 Conceptually, there are multiple version maps, each handling
188 different types of image. For instance, a single board may have
189 both a stable OS image (e.g. for CrOS), and a separate stable
190 firmware image.
191
192 Each different type of image requires a certain amount of special
193 handling, implemented by a subclass of `StableVersionMap`. The
194 subclasses take care of pre-processing of arguments, delegating
195 actual RPC calls to this superclass.
196
197 @property _afe AFE object through which to make the actual RPC
198 calls.
199 @property _android Value of the `android` parameter to be passed
200 when calling the `get_stable_version` RPC.
201 """
202
203 # DEFAULT_BOARD - The stable_version RPC API recognizes this special
204 # name as a mapping to use when no specific mapping for a board is
205 # present. This default mapping is only allowed for CrOS image
206 # types; other image type subclasses exclude it.
207 #
208 # TODO(jrbarnette): This value is copied from
209 # site_utils.stable_version_utils, because if we import that
210 # module here, it breaks unit tests. Something about the Django
211 # setup...
212 DEFAULT_BOARD = 'DEFAULT'
213
214
215 def __init__(self, afe, android):
216 self._afe = afe
217 self._android = android
218
219
220 def get_all_versions(self):
221 """
222 Get all mappings in the stable versions table.
223
224 Extracts the full content of the `stable_version` table
225 in the AFE database, and returns it as a dictionary
226 mapping board names to version strings.
227
228 @return A dictionary mapping board names to version strings.
229 """
230 return self._afe.run('get_all_stable_versions')
231
232
233 def get_version(self, board):
234 """
235 Get the mapping of one board in the stable versions table.
236
237 Look up and return the version mapped to the given board in the
238 `stable_versions` table in the AFE database.
239
240 @param board The board to be looked up.
241
242 @return The version mapped for the given board.
243 """
244 return self._afe.run('get_stable_version',
245 board=board, android=self._android)
246
247
248 def set_version(self, board, version):
249 """
250 Change the mapping of one board in the stable versions table.
251
252 Set the mapping in the `stable_versions` table in the AFE
253 database for the given board to the given version.
254
255 @param board The board to be updated.
256 @param version The new version to be assigned to the board.
257 """
258 self._afe.run('set_stable_version',
259 version=version, board=board)
260
261
262 def delete_version(self, board):
263 """
264 Remove the mapping of one board in the stable versions table.
265
266 Remove the mapping in the `stable_versions` table in the AFE
267 database for the given board.
268
269 @param board The board to be updated.
270 """
271 self._afe.run('delete_stable_version', board=board)
272
273
274class _OSVersionMap(_StableVersionMap):
275 """
276 Abstract stable version mapping for full OS images of various types.
277 """
278
279 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700280 # TODO(jrbarnette): We exclude non-OS (i.e. firmware) version
281 # mappings, but the returned dict doesn't distinguish CrOS
282 # boards from Android boards; both will be present, and the
283 # subclass can't distinguish them.
Richard Barnette260cbd02016-10-06 12:23:28 -0700284 #
285 # Ultimately, the right fix is to move knowledge of image type
286 # over to the RPC server side.
287 #
288 versions = super(_OSVersionMap, self).get_all_versions()
289 for board in versions.keys():
290 if '/' in board:
291 del versions[board]
292 return versions
293
294
295class _CrosVersionMap(_OSVersionMap):
296 """
297 Stable version mapping for Chrome OS release images.
298
299 This class manages a mapping of Chrome OS board names to known-good
300 release (or canary) images. The images selected can be installed on
301 DUTs during repair tasks, as a way of getting a DUT into a known
302 working state.
303 """
304
305 def __init__(self, afe):
306 super(_CrosVersionMap, self).__init__(afe, False)
307
Richard Barnette728e36f2016-11-03 16:04:29 -0700308 @staticmethod
309 def format_image_name(board, version):
310 """
311 Return an image name for a given `board` and `version`.
312
313 This formats `board` and `version` into a string identifying an
314 image file. The string represents part of a URL for access to
315 the image.
316
317 The returned image name is typically of a form like
318 "falco-release/R55-8872.44.0".
319 """
320 build_pattern = GLOBAL_CONFIG.get_config_value(
321 'CROS', 'stable_build_pattern')
322 return build_pattern % (board, version)
Richard Barnette260cbd02016-10-06 12:23:28 -0700323
Richard Barnette383ef9c2016-12-13 11:56:49 -0800324 def get_image_name(self, board):
325 """
326 Return the full image name of the stable version for `board`.
327
328 This finds the stable version for `board`, and returns a string
Richard Barnette728e36f2016-11-03 16:04:29 -0700329 identifying the associated image as for `format_image_name()`,
330 above.
Richard Barnette383ef9c2016-12-13 11:56:49 -0800331
332 @return A string identifying the image file for the stable
333 image for `board`.
334 """
Richard Barnette728e36f2016-11-03 16:04:29 -0700335 return self.format_image_name(board, self.get_version(board))
Richard Barnette260cbd02016-10-06 12:23:28 -0700336
337
338class _AndroidVersionMap(_OSVersionMap):
339 """
340 Stable version mapping for Android release images.
341
342 This class manages a mapping of Android/Brillo board names to
343 known-good images.
344 """
345
346 def __init__(self, afe):
347 super(_AndroidVersionMap, self).__init__(afe, True)
348
349
350 def get_all_versions(self):
351 versions = super(_AndroidVersionMap, self).get_all_versions()
352 del versions[self.DEFAULT_BOARD]
353 return versions
354
355
Richard Barnettee50453e2016-10-10 16:43:44 -0700356class _SuffixHackVersionMap(_StableVersionMap):
Richard Barnette260cbd02016-10-06 12:23:28 -0700357 """
Richard Barnettee50453e2016-10-10 16:43:44 -0700358 Abstract super class for mappings using a pseudo-board name.
Richard Barnette260cbd02016-10-06 12:23:28 -0700359
Richard Barnettee50453e2016-10-10 16:43:44 -0700360 For non-OS image type mappings, we look them up in the
361 `stable_versions` table by constructing a "pseudo-board" from the
362 real board name plus a suffix string that identifies the image type.
363 So, for instance the name "lulu/firmware" is used to look up the
364 FAFT firmware version for lulu boards.
Richard Barnette260cbd02016-10-06 12:23:28 -0700365 """
366
Richard Barnettee50453e2016-10-10 16:43:44 -0700367 # _SUFFIX - The suffix used in constructing the "pseudo-board"
368 # lookup key. Each subclass must define this value for itself.
Richard Barnette260cbd02016-10-06 12:23:28 -0700369 #
Richard Barnettee50453e2016-10-10 16:43:44 -0700370 _SUFFIX = None
Richard Barnette260cbd02016-10-06 12:23:28 -0700371
372 def __init__(self, afe):
Richard Barnettee50453e2016-10-10 16:43:44 -0700373 super(_SuffixHackVersionMap, self).__init__(afe, False)
Richard Barnette260cbd02016-10-06 12:23:28 -0700374
375
376 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700377 # Get all the mappings from the AFE, extract just the mappings
378 # with our suffix, and replace the pseudo-board name keys with
Richard Barnette260cbd02016-10-06 12:23:28 -0700379 # the real board names.
380 #
381 all_versions = super(
Richard Barnettee50453e2016-10-10 16:43:44 -0700382 _SuffixHackVersionMap, self).get_all_versions()
Richard Barnette260cbd02016-10-06 12:23:28 -0700383 return {
384 board[0 : -len(self._SUFFIX)]: all_versions[board]
385 for board in all_versions.keys()
386 if board.endswith(self._SUFFIX)
387 }
388
389
390 def get_version(self, board):
Richard Barnettee50453e2016-10-10 16:43:44 -0700391 board += self._SUFFIX
392 return super(_SuffixHackVersionMap, self).get_version(board)
393
394
395 def set_version(self, board, version):
396 board += self._SUFFIX
397 super(_SuffixHackVersionMap, self).set_version(board, version)
398
399
400 def delete_version(self, board):
401 board += self._SUFFIX
402 super(_SuffixHackVersionMap, self).delete_version(board)
403
404
405class _FAFTVersionMap(_SuffixHackVersionMap):
406 """
407 Stable version mapping for firmware versions used in FAFT repair.
408
409 When DUTs used for FAFT fail repair, stable firmware may need to be
410 flashed directly from original tarballs. The FAFT firmware version
411 mapping finds the appropriate tarball for a given board.
412 """
413
414 _SUFFIX = '/firmware'
415
416 def get_version(self, board):
417 # If there's no mapping for `board`, the lookup will return the
418 # default CrOS version mapping. To eliminate that case, we
419 # require a '/' character in the version, since CrOS versions
420 # won't match that.
Richard Barnette260cbd02016-10-06 12:23:28 -0700421 #
422 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
423 # the right fix is to move handling to the RPC server side.
424 #
Richard Barnette260cbd02016-10-06 12:23:28 -0700425 version = super(_FAFTVersionMap, self).get_version(board)
426 return version if '/' in version else None
427
428
Richard Barnettee50453e2016-10-10 16:43:44 -0700429class _FirmwareVersionMap(_SuffixHackVersionMap):
430 """
431 Stable version mapping for firmware supplied in Chrome OS images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700432
Richard Barnettee50453e2016-10-10 16:43:44 -0700433 A Chrome OS image bundles a version of the firmware that the
434 device should update to when the OS version is installed during
435 AU.
Richard Barnette260cbd02016-10-06 12:23:28 -0700436
Richard Barnettee50453e2016-10-10 16:43:44 -0700437 Test images suppress the firmware update during AU. Instead, during
438 repair and verify we check installed firmware on a DUT, compare it
439 against the stable version mapping for the board, and update when
440 the DUT is out-of-date.
441 """
442
443 _SUFFIX = '/rwfw'
444
445 def get_version(self, board):
446 # If there's no mapping for `board`, the lookup will return the
447 # default CrOS version mapping. To eliminate that case, we
448 # require the version start with "Google_", since CrOS versions
449 # won't match that.
450 #
451 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
452 # the right fix is to move handling to the RPC server side.
453 #
454 version = super(_FirmwareVersionMap, self).get_version(board)
455 return version if version.startswith('Google_') else None
Richard Barnette260cbd02016-10-06 12:23:28 -0700456
457
mbligh5280e3b2008-12-22 14:39:28 +0000458class AFE(RpcClient):
mbligh1ef218d2009-08-03 16:57:56 +0000459
Richard Barnette260cbd02016-10-06 12:23:28 -0700460 # Known image types for stable version mapping objects.
461 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
462 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
Richard Barnettee50453e2016-10-10 16:43:44 -0700463 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700464 # ANDROID_IMAGE_TYPE - Mappings for Android images.
465 #
466 CROS_IMAGE_TYPE = 'cros'
467 FAFT_IMAGE_TYPE = 'faft'
Richard Barnettee50453e2016-10-10 16:43:44 -0700468 FIRMWARE_IMAGE_TYPE = 'firmware'
Richard Barnette260cbd02016-10-06 12:23:28 -0700469 ANDROID_IMAGE_TYPE = 'android'
470
471 _IMAGE_MAPPING_CLASSES = {
472 CROS_IMAGE_TYPE: _CrosVersionMap,
473 FAFT_IMAGE_TYPE: _FAFTVersionMap,
Richard Barnettee50453e2016-10-10 16:43:44 -0700474 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
Richard Barnette260cbd02016-10-06 12:23:28 -0700475 ANDROID_IMAGE_TYPE: _AndroidVersionMap
476 }
477
478
Prathmesh Prabhu6d5ba592017-01-05 13:56:04 -0800479 def __init__(self, user=None, server=None, print_log=True, debug=False,
480 reply_debug=False, job=None):
481 self.job = job
482 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
483 user=user,
484 server=server,
485 print_log=print_log,
486 debug=debug,
487 reply_debug=reply_debug)
488
489
Richard Barnette260cbd02016-10-06 12:23:28 -0700490 def get_stable_version_map(self, image_type):
491 """
492 Return a stable version mapping for the given image type.
493
494 @return An object mapping board names to version strings for
495 software of the given image type.
496 """
497 return self._IMAGE_MAPPING_CLASSES[image_type](self)
498
499
mbligh67647152008-11-19 00:18:14 +0000500 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000501 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000502 statuses = self.run('get_static_data')['host_statuses']
503 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000504 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000505 if live == False:
506 return dead_statuses
507 else:
508 return statuses
509
510
mbligh71094012009-12-19 05:35:21 +0000511 @staticmethod
512 def _dict_for_host_query(hostnames=(), status=None, label=None):
513 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000514 if hostnames:
515 query_args['hostname__in'] = hostnames
516 if status:
517 query_args['status'] = status
518 if label:
519 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000520 return query_args
521
522
523 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
524 query_args = dict(dargs)
525 query_args.update(self._dict_for_host_query(hostnames=hostnames,
526 status=status,
527 label=label))
528 hosts = self.run('get_hosts', **query_args)
529 return [Host(self, h) for h in hosts]
530
531
532 def get_hostnames(self, status=None, label=None, **dargs):
533 """Like get_hosts() but returns hostnames instead of Host objects."""
534 # This implementation can be replaced with a more efficient one
535 # that does not query for entire host objects in the future.
536 return [host_obj.hostname for host_obj in
537 self.get_hosts(status=status, label=label, **dargs)]
538
539
540 def reverify_hosts(self, hostnames=(), status=None, label=None):
541 query_args = dict(locked=False,
542 aclgroup__users__login=self.user)
543 query_args.update(self._dict_for_host_query(hostnames=hostnames,
544 status=status,
545 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000546 return self.run('reverify_hosts', **query_args)
547
548
Richard Barnette73e74b02017-08-10 15:16:49 -0700549 def repair_hosts(self, hostnames=(), status=None, label=None):
550 query_args = dict(locked=False,
551 aclgroup__users__login=self.user)
552 query_args.update(self._dict_for_host_query(hostnames=hostnames,
553 status=status,
554 label=label))
555 return self.run('repair_hosts', **query_args)
556
557
mbligh67647152008-11-19 00:18:14 +0000558 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000559 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000560 return self.get_hosts(id=id)[0]
561
562
MK Ryuacf35922014-10-03 14:56:49 -0700563 def get_host_attribute(self, attr, **dargs):
564 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
565 return [HostAttribute(self, a) for a in host_attrs]
566
567
Chris Masone8abb6fc2012-01-31 09:27:36 -0800568 def set_host_attribute(self, attr, val, **dargs):
569 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
570
571
mbligh67647152008-11-19 00:18:14 +0000572 def get_labels(self, **dargs):
573 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000574 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000575
576
577 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000578 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000579 return self.get_labels(id=id)[0]
580
581
582 def get_acls(self, **dargs):
583 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000584 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000585
586
587 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000588 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000589 return self.get_acls(id=id)[0]
590
591
mbligh54459c72009-01-21 19:26:44 +0000592 def get_users(self, **dargs):
593 users = self.run('get_users', **dargs)
594 return [User(self, u) for u in users]
595
596
mbligh1354c9d2008-12-22 14:56:13 +0000597 def generate_control_file(self, tests, **dargs):
598 ret = self.run('generate_control_file', tests=tests, **dargs)
599 return ControlFile(self, ret)
600
601
mbligh67647152008-11-19 00:18:14 +0000602 def get_jobs(self, summary=False, **dargs):
603 if summary:
604 jobs_data = self.run('get_jobs_summary', **dargs)
605 else:
606 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000607 jobs = []
608 for j in jobs_data:
609 job = Job(self, j)
610 # Set up some extra information defaults
611 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
612 job.platform_results = {}
613 job.platform_reasons = {}
614 jobs.append(job)
615 return jobs
mbligh67647152008-11-19 00:18:14 +0000616
617
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700618 def get_host_queue_entries(self, **kwargs):
619 """Find JobStatus objects matching some constraints.
mbligh99b24f42009-06-08 16:45:55 +0000620
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700621 @param **kwargs: Arguments to pass to the RPC
622 """
623 entries = self.run('get_host_queue_entries', **kwargs)
624 return self._entries_to_statuses(entries)
625
626
627 def get_host_queue_entries_by_insert_time(self, **kwargs):
628 """Like get_host_queue_entries, but using the insert index table.
629
630 @param **kwargs: Arguments to pass to the RPC
631 """
632 entries = self.run('get_host_queue_entries_by_insert_time', **kwargs)
633 return self._entries_to_statuses(entries)
634
635
636 def _entries_to_statuses(self, entries):
637 """Converts HQEs to JobStatuses
638
639 Sadly, get_host_queue_entries doesn't return platforms, we have
640 to get those back from an explicit get_hosts queury, then patch
641 the new host objects back into the host list.
642
643 :param entries: A list of HQEs from get_host_queue_entries or
644 get_host_queue_entries_by_insert_time.
645 """
646 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000647 hostnames = [s.host.hostname for s in job_statuses if s.host]
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700648 hosts = {}
mbligh99b24f42009-06-08 16:45:55 +0000649 for host in self.get_hosts(hostname__in=hostnames):
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700650 hosts[host.hostname] = host
mbligh99b24f42009-06-08 16:45:55 +0000651 for status in job_statuses:
652 if status.host:
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700653 status.host = hosts.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000654 # filter job statuses that have either host or meta_host
655 return [status for status in job_statuses if (status.host or
656 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000657
658
MK Ryu1b2d7f92015-02-24 17:45:02 -0800659 def get_special_tasks(self, **data):
660 tasks = self.run('get_special_tasks', **data)
661 return [SpecialTask(self, t) for t in tasks]
662
663
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700664 def get_host_special_tasks(self, host_id, **data):
665 tasks = self.run('get_host_special_tasks',
666 host_id=host_id, **data)
667 return [SpecialTask(self, t) for t in tasks]
668
669
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700670 def get_host_status_task(self, host_id, end_time):
671 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700672 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700673 return SpecialTask(self, task) if task else None
674
675
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700676 def get_host_diagnosis_interval(self, host_id, end_time, success):
677 return self.run('get_host_diagnosis_interval',
678 host_id=host_id, end_time=end_time,
679 success=success)
680
681
Allen Li352b86a2016-12-14 12:11:27 -0800682 def create_job(self, control_file, name=' ',
683 priority=priorities.Priority.DEFAULT,
684 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT,
685 **dargs):
mbligh67647152008-11-19 00:18:14 +0000686 id = self.run('create_job', name=name, priority=priority,
687 control_file=control_file, control_type=control_type, **dargs)
688 return self.get_jobs(id=id)[0]
689
690
Simran Basi01984f52015-10-12 15:36:45 -0700691 def abort_jobs(self, jobs):
692 """Abort a list of jobs.
693
694 Already completed jobs will not be affected.
695
696 @param jobs: List of job ids to abort.
697 """
698 for job in jobs:
699 self.run('abort_host_queue_entries', job_id=job)
700
701
Kevin Cheng19521982016-09-22 12:27:23 -0700702 def get_hosts_by_attribute(self, attribute, value):
703 """
704 Get the list of hosts that share the same host attribute value.
705
706 @param attribute: String of the host attribute to check.
707 @param value: String of the value that is shared between hosts.
708
709 @returns List of hostnames that all have the same host attribute and
710 value.
711 """
712 return self.run('get_hosts_by_attribute',
713 attribute=attribute, value=value)
714
715
716 def lock_host(self, host, lock_reason, fail_if_locked=False):
717 """
718 Lock the given host with the given lock reason.
719
720 Locking a host that's already locked using the 'modify_hosts' rpc
721 will raise an exception. That's why fail_if_locked exists so the
722 caller can determine if the lock succeeded or failed. This will
723 save every caller from wrapping lock_host in a try-except.
724
725 @param host: hostname of host to lock.
726 @param lock_reason: Reason for locking host.
727 @param fail_if_locked: Return False if host is already locked.
728
729 @returns Boolean, True if lock was successful, False otherwise.
730 """
731 try:
732 self.run('modify_hosts',
733 host_filter_data={'hostname': host},
734 update_data={'locked': True,
735 'lock_reason': lock_reason})
736 except Exception:
737 return not fail_if_locked
738 return True
739
740
741 def unlock_hosts(self, locked_hosts):
742 """
743 Unlock the hosts.
744
745 Unlocking a host that's already unlocked will do nothing so we don't
746 need any special try-except clause here.
747
748 @param locked_hosts: List of hostnames of hosts to unlock.
749 """
750 self.run('modify_hosts',
751 host_filter_data={'hostname__in': locked_hosts},
752 update_data={'locked': False,
753 'lock_reason': ''})
754
755
mbligh5280e3b2008-12-22 14:39:28 +0000756class TestResults(object):
757 """
758 Container class used to hold the results of the tests for a job
759 """
760 def __init__(self):
761 self.good = []
762 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000763 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000764
765
766 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000767 if result.complete_count > result.pass_count:
768 self.fail.append(result)
769 elif result.incomplete_count > 0:
770 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000771 else:
mbligh451ede12009-02-12 21:54:03 +0000772 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000773
774
775class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000776 """
777 Generic object used to construct python objects from rpc calls
778 """
779 def __init__(self, afe, hash):
780 self.afe = afe
781 self.hash = hash
782 self.__dict__.update(hash)
783
784
785 def __str__(self):
786 return dump_object(self.__repr__(), self)
787
788
mbligh1354c9d2008-12-22 14:56:13 +0000789class ControlFile(RpcObject):
790 """
791 AFE control file object
792
793 Fields: synch_count, dependencies, control_file, is_server
794 """
795 def __repr__(self):
796 return 'CONTROL FILE: %s' % self.control_file
797
798
mbligh5280e3b2008-12-22 14:39:28 +0000799class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000800 """
801 AFE label object
802
803 Fields:
804 name, invalid, platform, kernel_config, id, only_if_needed
805 """
806 def __repr__(self):
807 return 'LABEL: %s' % self.name
808
809
810 def add_hosts(self, hosts):
Prathmesh Prabhuef7e5b02017-11-08 11:57:47 -0800811 # We must use the label's name instead of the id because label ids are
812 # not consistent across master-shard.
813 return self.afe.run('label_add_hosts', id=self.name, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000814
815
816 def remove_hosts(self, hosts):
Prathmesh Prabhuef7e5b02017-11-08 11:57:47 -0800817 # We must use the label's name instead of the id because label ids are
818 # not consistent across master-shard.
819 return self.afe.run('label_remove_hosts', id=self.name, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000820
821
mbligh5280e3b2008-12-22 14:39:28 +0000822class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000823 """
824 AFE acl object
825
826 Fields:
827 users, hosts, description, name, id
828 """
829 def __repr__(self):
830 return 'ACL: %s' % self.name
831
832
833 def add_hosts(self, hosts):
834 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
835 return self.afe.run('acl_group_add_hosts', self.id, hosts)
836
837
838 def remove_hosts(self, hosts):
839 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
840 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
841
842
mbligh54459c72009-01-21 19:26:44 +0000843 def add_users(self, users):
844 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
845 return self.afe.run('acl_group_add_users', id=self.name, users=users)
846
847
mbligh5280e3b2008-12-22 14:39:28 +0000848class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000849 """
850 AFE job object
851
852 Fields:
853 name, control_file, control_type, synch_count, reboot_before,
854 run_verify, priority, email_list, created_on, dependencies,
855 timeout, owner, reboot_after, id
856 """
857 def __repr__(self):
858 return 'JOB: %s' % self.id
859
860
mbligh5280e3b2008-12-22 14:39:28 +0000861class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000862 """
863 AFE job_status object
864
865 Fields:
866 status, complete, deleted, meta_host, host, active, execution_subdir, id
867 """
868 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800869 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000870 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700871 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000872 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000873
874
875 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000876 if self.host and self.host.hostname:
877 hostname = self.host.hostname
878 else:
879 hostname = 'None'
880 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000881
882
MK Ryu1b2d7f92015-02-24 17:45:02 -0800883class SpecialTask(RpcObject):
884 """
885 AFE special task object
886 """
887 def __init__(self, afe, hash):
888 super(SpecialTask, self).__init__(afe, hash)
889 self.host = Host(afe, self.host)
890
891
892 def __repr__(self):
893 return 'SPECIAL TASK: %s' % self.id
894
895
mbligh5280e3b2008-12-22 14:39:28 +0000896class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000897 """
898 AFE host object
899
900 Fields:
901 status, lock_time, locked_by, locked, hostname, invalid,
Allen Lie4c08272017-02-01 16:40:53 -0800902 labels, platform, protection, dirty, id
mbligh67647152008-11-19 00:18:14 +0000903 """
904 def __repr__(self):
905 return 'HOST OBJECT: %s' % self.hostname
906
907
908 def show(self):
909 labels = list(set(self.labels) - set([self.platform]))
910 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
911 self.locked, self.platform,
912 ', '.join(labels))
913
914
mbligh54459c72009-01-21 19:26:44 +0000915 def delete(self):
916 return self.afe.run('delete_host', id=self.id)
917
918
mbligh6463c4b2009-01-30 00:33:37 +0000919 def modify(self, **dargs):
920 return self.afe.run('modify_host', id=self.id, **dargs)
921
922
mbligh67647152008-11-19 00:18:14 +0000923 def get_acls(self):
924 return self.afe.get_acls(hosts__hostname=self.hostname)
925
926
927 def add_acl(self, acl_name):
928 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
929 return self.afe.run('acl_group_add_hosts', id=acl_name,
930 hosts=[self.hostname])
931
932
933 def remove_acl(self, acl_name):
934 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
935 return self.afe.run('acl_group_remove_hosts', id=acl_name,
936 hosts=[self.hostname])
937
938
939 def get_labels(self):
940 return self.afe.get_labels(host__hostname__in=[self.hostname])
941
942
943 def add_labels(self, labels):
944 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
945 return self.afe.run('host_add_labels', id=self.id, labels=labels)
946
947
948 def remove_labels(self, labels):
949 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
950 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000951
952
Allen Lif74957d2017-11-20 17:46:48 -0800953 def is_available(self):
954 """Check whether DUT host is available.
955
956 @return: bool
957 """
958 return not (self.locked
959 or self.status in host_states.UNAVAILABLE_STATES)
960
961
mbligh54459c72009-01-21 19:26:44 +0000962class User(RpcObject):
963 def __repr__(self):
964 return 'USER: %s' % self.login
965
966
mbligh5280e3b2008-12-22 14:39:28 +0000967class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000968 """
969 TKO test status object
970
971 Fields:
972 test_idx, hostname, testname, id
973 complete_count, incomplete_count, group_count, pass_count
974 """
975 def __repr__(self):
976 return 'TEST STATUS: %s' % self.id
977
978
MK Ryuacf35922014-10-03 14:56:49 -0700979class HostAttribute(RpcObject):
980 """
981 AFE host attribute object
982
983 Fields:
984 id, host, attribute, value
985 """
986 def __repr__(self):
987 return 'HOST ATTRIBUTE %d' % self.id