blob: 25aeba37b0765aa38f5189e3d7942a27bd034910 [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}
Prathmesh Prabhu2892ff62017-12-19 10:21:31 -080093 rpc_server = rpc_client_lib.add_protocol(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
Richard Barnette260cbd02016-10-06 12:23:28 -0700203 def __init__(self, afe, android):
204 self._afe = afe
205 self._android = android
206
207
208 def get_all_versions(self):
209 """
210 Get all mappings in the stable versions table.
211
212 Extracts the full content of the `stable_version` table
213 in the AFE database, and returns it as a dictionary
214 mapping board names to version strings.
215
216 @return A dictionary mapping board names to version strings.
217 """
218 return self._afe.run('get_all_stable_versions')
219
220
221 def get_version(self, board):
222 """
223 Get the mapping of one board in the stable versions table.
224
225 Look up and return the version mapped to the given board in the
226 `stable_versions` table in the AFE database.
227
228 @param board The board to be looked up.
229
230 @return The version mapped for the given board.
231 """
232 return self._afe.run('get_stable_version',
233 board=board, android=self._android)
234
235
236 def set_version(self, board, version):
237 """
238 Change the mapping of one board in the stable versions table.
239
240 Set the mapping in the `stable_versions` table in the AFE
241 database for the given board to the given version.
242
243 @param board The board to be updated.
244 @param version The new version to be assigned to the board.
245 """
246 self._afe.run('set_stable_version',
247 version=version, board=board)
248
249
250 def delete_version(self, board):
251 """
252 Remove the mapping of one board in the stable versions table.
253
254 Remove the mapping in the `stable_versions` table in the AFE
255 database for the given board.
256
257 @param board The board to be updated.
258 """
259 self._afe.run('delete_stable_version', board=board)
260
261
262class _OSVersionMap(_StableVersionMap):
263 """
264 Abstract stable version mapping for full OS images of various types.
265 """
266
Richard Barnette206b25f2018-04-03 13:53:22 -0700267 def _version_is_valid(self, version):
268 return True
269
Richard Barnette260cbd02016-10-06 12:23:28 -0700270 def get_all_versions(self):
Richard Barnette260cbd02016-10-06 12:23:28 -0700271 versions = super(_OSVersionMap, self).get_all_versions()
272 for board in versions.keys():
Richard Barnette206b25f2018-04-03 13:53:22 -0700273 if ('/' in board
274 or not self._version_is_valid(versions[board])):
Richard Barnette260cbd02016-10-06 12:23:28 -0700275 del versions[board]
276 return versions
277
Richard Barnette206b25f2018-04-03 13:53:22 -0700278 def get_version(self, board):
279 version = super(_OSVersionMap, self).get_version(board)
280 return version if self._version_is_valid(version) else None
281
Richard Barnette260cbd02016-10-06 12:23:28 -0700282
Richard Barnette08e487d2018-03-30 18:39:31 -0700283def format_cros_image_name(board, version):
284 """
285 Return an image name for a given `board` and `version`.
286
287 This formats `board` and `version` into a string identifying an
288 image file. The string represents part of a URL for access to
289 the image.
290
291 The returned image name is typically of a form like
292 "falco-release/R55-8872.44.0".
293 """
294 build_pattern = GLOBAL_CONFIG.get_config_value(
295 'CROS', 'stable_build_pattern')
296 return build_pattern % (board, version)
297
298
Richard Barnette260cbd02016-10-06 12:23:28 -0700299class _CrosVersionMap(_OSVersionMap):
300 """
301 Stable version mapping for Chrome OS release images.
302
303 This class manages a mapping of Chrome OS board names to known-good
304 release (or canary) images. The images selected can be installed on
305 DUTs during repair tasks, as a way of getting a DUT into a known
306 working state.
307 """
308
309 def __init__(self, afe):
310 super(_CrosVersionMap, self).__init__(afe, False)
311
Richard Barnette206b25f2018-04-03 13:53:22 -0700312 def _version_is_valid(self, version):
313 return version is not None and '/' not in version
314
Richard Barnette383ef9c2016-12-13 11:56:49 -0800315 def get_image_name(self, board):
316 """
317 Return the full image name of the stable version for `board`.
318
319 This finds the stable version for `board`, and returns a string
Richard Barnette728e36f2016-11-03 16:04:29 -0700320 identifying the associated image as for `format_image_name()`,
321 above.
Richard Barnette383ef9c2016-12-13 11:56:49 -0800322
323 @return A string identifying the image file for the stable
324 image for `board`.
325 """
Richard Barnette08e487d2018-03-30 18:39:31 -0700326 return format_cros_image_name(board, self.get_version(board))
Richard Barnette260cbd02016-10-06 12:23:28 -0700327
328
329class _AndroidVersionMap(_OSVersionMap):
330 """
331 Stable version mapping for Android release images.
332
333 This class manages a mapping of Android/Brillo board names to
334 known-good images.
335 """
336
337 def __init__(self, afe):
338 super(_AndroidVersionMap, self).__init__(afe, True)
339
Richard Barnette206b25f2018-04-03 13:53:22 -0700340 def _version_is_valid(self, version):
341 return version is not None and '/' in version
Richard Barnette260cbd02016-10-06 12:23:28 -0700342
343
Richard Barnettee50453e2016-10-10 16:43:44 -0700344class _SuffixHackVersionMap(_StableVersionMap):
Richard Barnette260cbd02016-10-06 12:23:28 -0700345 """
Richard Barnettee50453e2016-10-10 16:43:44 -0700346 Abstract super class for mappings using a pseudo-board name.
Richard Barnette260cbd02016-10-06 12:23:28 -0700347
Richard Barnettee50453e2016-10-10 16:43:44 -0700348 For non-OS image type mappings, we look them up in the
349 `stable_versions` table by constructing a "pseudo-board" from the
350 real board name plus a suffix string that identifies the image type.
351 So, for instance the name "lulu/firmware" is used to look up the
352 FAFT firmware version for lulu boards.
Richard Barnette260cbd02016-10-06 12:23:28 -0700353 """
354
Richard Barnettee50453e2016-10-10 16:43:44 -0700355 # _SUFFIX - The suffix used in constructing the "pseudo-board"
356 # lookup key. Each subclass must define this value for itself.
Richard Barnette260cbd02016-10-06 12:23:28 -0700357 #
Richard Barnettee50453e2016-10-10 16:43:44 -0700358 _SUFFIX = None
Richard Barnette260cbd02016-10-06 12:23:28 -0700359
360 def __init__(self, afe):
Richard Barnettee50453e2016-10-10 16:43:44 -0700361 super(_SuffixHackVersionMap, self).__init__(afe, False)
Richard Barnette260cbd02016-10-06 12:23:28 -0700362
363
364 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700365 # Get all the mappings from the AFE, extract just the mappings
366 # with our suffix, and replace the pseudo-board name keys with
Richard Barnette260cbd02016-10-06 12:23:28 -0700367 # the real board names.
368 #
369 all_versions = super(
Richard Barnettee50453e2016-10-10 16:43:44 -0700370 _SuffixHackVersionMap, self).get_all_versions()
Richard Barnette260cbd02016-10-06 12:23:28 -0700371 return {
372 board[0 : -len(self._SUFFIX)]: all_versions[board]
373 for board in all_versions.keys()
374 if board.endswith(self._SUFFIX)
375 }
376
377
378 def get_version(self, board):
Richard Barnettee50453e2016-10-10 16:43:44 -0700379 board += self._SUFFIX
380 return super(_SuffixHackVersionMap, self).get_version(board)
381
382
383 def set_version(self, board, version):
384 board += self._SUFFIX
385 super(_SuffixHackVersionMap, self).set_version(board, version)
386
387
388 def delete_version(self, board):
389 board += self._SUFFIX
390 super(_SuffixHackVersionMap, self).delete_version(board)
391
392
393class _FAFTVersionMap(_SuffixHackVersionMap):
394 """
395 Stable version mapping for firmware versions used in FAFT repair.
396
397 When DUTs used for FAFT fail repair, stable firmware may need to be
398 flashed directly from original tarballs. The FAFT firmware version
399 mapping finds the appropriate tarball for a given board.
400 """
401
402 _SUFFIX = '/firmware'
403
404 def get_version(self, board):
405 # If there's no mapping for `board`, the lookup will return the
406 # default CrOS version mapping. To eliminate that case, we
407 # require a '/' character in the version, since CrOS versions
408 # won't match that.
Richard Barnette260cbd02016-10-06 12:23:28 -0700409 #
410 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
411 # the right fix is to move handling to the RPC server side.
412 #
Richard Barnette260cbd02016-10-06 12:23:28 -0700413 version = super(_FAFTVersionMap, self).get_version(board)
414 return version if '/' in version else None
415
416
Richard Barnettee50453e2016-10-10 16:43:44 -0700417class _FirmwareVersionMap(_SuffixHackVersionMap):
418 """
419 Stable version mapping for firmware supplied in Chrome OS images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700420
Richard Barnettee50453e2016-10-10 16:43:44 -0700421 A Chrome OS image bundles a version of the firmware that the
422 device should update to when the OS version is installed during
423 AU.
Richard Barnette260cbd02016-10-06 12:23:28 -0700424
Richard Barnettee50453e2016-10-10 16:43:44 -0700425 Test images suppress the firmware update during AU. Instead, during
426 repair and verify we check installed firmware on a DUT, compare it
427 against the stable version mapping for the board, and update when
428 the DUT is out-of-date.
429 """
430
431 _SUFFIX = '/rwfw'
432
433 def get_version(self, board):
434 # If there's no mapping for `board`, the lookup will return the
435 # default CrOS version mapping. To eliminate that case, we
436 # require the version start with "Google_", since CrOS versions
437 # won't match that.
438 #
439 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
440 # the right fix is to move handling to the RPC server side.
441 #
442 version = super(_FirmwareVersionMap, self).get_version(board)
443 return version if version.startswith('Google_') else None
Richard Barnette260cbd02016-10-06 12:23:28 -0700444
445
mbligh5280e3b2008-12-22 14:39:28 +0000446class AFE(RpcClient):
mbligh1ef218d2009-08-03 16:57:56 +0000447
Richard Barnette260cbd02016-10-06 12:23:28 -0700448 # Known image types for stable version mapping objects.
449 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
450 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
Richard Barnettee50453e2016-10-10 16:43:44 -0700451 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700452 # ANDROID_IMAGE_TYPE - Mappings for Android images.
453 #
454 CROS_IMAGE_TYPE = 'cros'
455 FAFT_IMAGE_TYPE = 'faft'
Richard Barnettee50453e2016-10-10 16:43:44 -0700456 FIRMWARE_IMAGE_TYPE = 'firmware'
Richard Barnette260cbd02016-10-06 12:23:28 -0700457 ANDROID_IMAGE_TYPE = 'android'
458
459 _IMAGE_MAPPING_CLASSES = {
460 CROS_IMAGE_TYPE: _CrosVersionMap,
461 FAFT_IMAGE_TYPE: _FAFTVersionMap,
Richard Barnettee50453e2016-10-10 16:43:44 -0700462 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
Richard Barnette260cbd02016-10-06 12:23:28 -0700463 ANDROID_IMAGE_TYPE: _AndroidVersionMap
464 }
465
466
Prathmesh Prabhu6d5ba592017-01-05 13:56:04 -0800467 def __init__(self, user=None, server=None, print_log=True, debug=False,
468 reply_debug=False, job=None):
469 self.job = job
470 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
471 user=user,
472 server=server,
473 print_log=print_log,
474 debug=debug,
475 reply_debug=reply_debug)
476
477
Richard Barnette260cbd02016-10-06 12:23:28 -0700478 def get_stable_version_map(self, image_type):
479 """
480 Return a stable version mapping for the given image type.
481
482 @return An object mapping board names to version strings for
483 software of the given image type.
484 """
485 return self._IMAGE_MAPPING_CLASSES[image_type](self)
486
487
mbligh67647152008-11-19 00:18:14 +0000488 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000489 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000490 statuses = self.run('get_static_data')['host_statuses']
491 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000492 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000493 if live == False:
494 return dead_statuses
495 else:
496 return statuses
497
498
mbligh71094012009-12-19 05:35:21 +0000499 @staticmethod
500 def _dict_for_host_query(hostnames=(), status=None, label=None):
501 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000502 if hostnames:
503 query_args['hostname__in'] = hostnames
504 if status:
505 query_args['status'] = status
506 if label:
507 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000508 return query_args
509
510
511 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
512 query_args = dict(dargs)
513 query_args.update(self._dict_for_host_query(hostnames=hostnames,
514 status=status,
515 label=label))
516 hosts = self.run('get_hosts', **query_args)
517 return [Host(self, h) for h in hosts]
518
519
520 def get_hostnames(self, status=None, label=None, **dargs):
521 """Like get_hosts() but returns hostnames instead of Host objects."""
522 # This implementation can be replaced with a more efficient one
523 # that does not query for entire host objects in the future.
524 return [host_obj.hostname for host_obj in
525 self.get_hosts(status=status, label=label, **dargs)]
526
527
528 def reverify_hosts(self, hostnames=(), status=None, label=None):
529 query_args = dict(locked=False,
530 aclgroup__users__login=self.user)
531 query_args.update(self._dict_for_host_query(hostnames=hostnames,
532 status=status,
533 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000534 return self.run('reverify_hosts', **query_args)
535
536
Richard Barnette73e74b02017-08-10 15:16:49 -0700537 def repair_hosts(self, hostnames=(), status=None, label=None):
538 query_args = dict(locked=False,
539 aclgroup__users__login=self.user)
540 query_args.update(self._dict_for_host_query(hostnames=hostnames,
541 status=status,
542 label=label))
543 return self.run('repair_hosts', **query_args)
544
545
mbligh67647152008-11-19 00:18:14 +0000546 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000547 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000548 return self.get_hosts(id=id)[0]
549
550
MK Ryuacf35922014-10-03 14:56:49 -0700551 def get_host_attribute(self, attr, **dargs):
552 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
553 return [HostAttribute(self, a) for a in host_attrs]
554
555
Chris Masone8abb6fc2012-01-31 09:27:36 -0800556 def set_host_attribute(self, attr, val, **dargs):
557 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
558
559
mbligh67647152008-11-19 00:18:14 +0000560 def get_labels(self, **dargs):
561 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000562 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000563
564
565 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000566 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000567 return self.get_labels(id=id)[0]
568
569
570 def get_acls(self, **dargs):
571 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000572 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000573
574
575 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000576 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000577 return self.get_acls(id=id)[0]
578
579
mbligh54459c72009-01-21 19:26:44 +0000580 def get_users(self, **dargs):
581 users = self.run('get_users', **dargs)
582 return [User(self, u) for u in users]
583
584
mbligh1354c9d2008-12-22 14:56:13 +0000585 def generate_control_file(self, tests, **dargs):
586 ret = self.run('generate_control_file', tests=tests, **dargs)
587 return ControlFile(self, ret)
588
589
mbligh67647152008-11-19 00:18:14 +0000590 def get_jobs(self, summary=False, **dargs):
591 if summary:
592 jobs_data = self.run('get_jobs_summary', **dargs)
593 else:
594 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000595 jobs = []
596 for j in jobs_data:
597 job = Job(self, j)
598 # Set up some extra information defaults
599 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
600 job.platform_results = {}
601 job.platform_reasons = {}
602 jobs.append(job)
603 return jobs
mbligh67647152008-11-19 00:18:14 +0000604
605
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700606 def get_host_queue_entries(self, **kwargs):
607 """Find JobStatus objects matching some constraints.
mbligh99b24f42009-06-08 16:45:55 +0000608
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700609 @param **kwargs: Arguments to pass to the RPC
610 """
611 entries = self.run('get_host_queue_entries', **kwargs)
612 return self._entries_to_statuses(entries)
613
614
615 def get_host_queue_entries_by_insert_time(self, **kwargs):
616 """Like get_host_queue_entries, but using the insert index table.
617
618 @param **kwargs: Arguments to pass to the RPC
619 """
620 entries = self.run('get_host_queue_entries_by_insert_time', **kwargs)
621 return self._entries_to_statuses(entries)
622
623
624 def _entries_to_statuses(self, entries):
625 """Converts HQEs to JobStatuses
626
627 Sadly, get_host_queue_entries doesn't return platforms, we have
628 to get those back from an explicit get_hosts queury, then patch
629 the new host objects back into the host list.
630
631 :param entries: A list of HQEs from get_host_queue_entries or
632 get_host_queue_entries_by_insert_time.
633 """
634 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000635 hostnames = [s.host.hostname for s in job_statuses if s.host]
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700636 hosts = {}
mbligh99b24f42009-06-08 16:45:55 +0000637 for host in self.get_hosts(hostname__in=hostnames):
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700638 hosts[host.hostname] = host
mbligh99b24f42009-06-08 16:45:55 +0000639 for status in job_statuses:
640 if status.host:
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700641 status.host = hosts.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000642 # filter job statuses that have either host or meta_host
643 return [status for status in job_statuses if (status.host or
644 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000645
646
MK Ryu1b2d7f92015-02-24 17:45:02 -0800647 def get_special_tasks(self, **data):
648 tasks = self.run('get_special_tasks', **data)
649 return [SpecialTask(self, t) for t in tasks]
650
651
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700652 def get_host_special_tasks(self, host_id, **data):
653 tasks = self.run('get_host_special_tasks',
654 host_id=host_id, **data)
655 return [SpecialTask(self, t) for t in tasks]
656
657
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700658 def get_host_status_task(self, host_id, end_time):
659 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700660 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700661 return SpecialTask(self, task) if task else None
662
663
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700664 def get_host_diagnosis_interval(self, host_id, end_time, success):
665 return self.run('get_host_diagnosis_interval',
666 host_id=host_id, end_time=end_time,
667 success=success)
668
669
Allen Li352b86a2016-12-14 12:11:27 -0800670 def create_job(self, control_file, name=' ',
671 priority=priorities.Priority.DEFAULT,
672 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT,
673 **dargs):
mbligh67647152008-11-19 00:18:14 +0000674 id = self.run('create_job', name=name, priority=priority,
675 control_file=control_file, control_type=control_type, **dargs)
676 return self.get_jobs(id=id)[0]
677
678
Simran Basi01984f52015-10-12 15:36:45 -0700679 def abort_jobs(self, jobs):
680 """Abort a list of jobs.
681
682 Already completed jobs will not be affected.
683
684 @param jobs: List of job ids to abort.
685 """
686 for job in jobs:
687 self.run('abort_host_queue_entries', job_id=job)
688
689
Kevin Cheng19521982016-09-22 12:27:23 -0700690 def get_hosts_by_attribute(self, attribute, value):
691 """
692 Get the list of hosts that share the same host attribute value.
693
694 @param attribute: String of the host attribute to check.
695 @param value: String of the value that is shared between hosts.
696
697 @returns List of hostnames that all have the same host attribute and
698 value.
699 """
700 return self.run('get_hosts_by_attribute',
701 attribute=attribute, value=value)
702
703
704 def lock_host(self, host, lock_reason, fail_if_locked=False):
705 """
706 Lock the given host with the given lock reason.
707
708 Locking a host that's already locked using the 'modify_hosts' rpc
709 will raise an exception. That's why fail_if_locked exists so the
710 caller can determine if the lock succeeded or failed. This will
711 save every caller from wrapping lock_host in a try-except.
712
713 @param host: hostname of host to lock.
714 @param lock_reason: Reason for locking host.
715 @param fail_if_locked: Return False if host is already locked.
716
717 @returns Boolean, True if lock was successful, False otherwise.
718 """
719 try:
720 self.run('modify_hosts',
721 host_filter_data={'hostname': host},
722 update_data={'locked': True,
723 'lock_reason': lock_reason})
724 except Exception:
725 return not fail_if_locked
726 return True
727
728
729 def unlock_hosts(self, locked_hosts):
730 """
731 Unlock the hosts.
732
733 Unlocking a host that's already unlocked will do nothing so we don't
734 need any special try-except clause here.
735
736 @param locked_hosts: List of hostnames of hosts to unlock.
737 """
738 self.run('modify_hosts',
739 host_filter_data={'hostname__in': locked_hosts},
740 update_data={'locked': False,
741 'lock_reason': ''})
742
743
mbligh5280e3b2008-12-22 14:39:28 +0000744class TestResults(object):
745 """
746 Container class used to hold the results of the tests for a job
747 """
748 def __init__(self):
749 self.good = []
750 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000751 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000752
753
754 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000755 if result.complete_count > result.pass_count:
756 self.fail.append(result)
757 elif result.incomplete_count > 0:
758 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000759 else:
mbligh451ede12009-02-12 21:54:03 +0000760 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000761
762
763class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000764 """
765 Generic object used to construct python objects from rpc calls
766 """
767 def __init__(self, afe, hash):
768 self.afe = afe
769 self.hash = hash
770 self.__dict__.update(hash)
771
772
773 def __str__(self):
774 return dump_object(self.__repr__(), self)
775
776
mbligh1354c9d2008-12-22 14:56:13 +0000777class ControlFile(RpcObject):
778 """
779 AFE control file object
780
781 Fields: synch_count, dependencies, control_file, is_server
782 """
783 def __repr__(self):
784 return 'CONTROL FILE: %s' % self.control_file
785
786
mbligh5280e3b2008-12-22 14:39:28 +0000787class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000788 """
789 AFE label object
790
791 Fields:
792 name, invalid, platform, kernel_config, id, only_if_needed
793 """
794 def __repr__(self):
795 return 'LABEL: %s' % self.name
796
797
798 def add_hosts(self, hosts):
Prathmesh Prabhuef7e5b02017-11-08 11:57:47 -0800799 # We must use the label's name instead of the id because label ids are
800 # not consistent across master-shard.
801 return self.afe.run('label_add_hosts', id=self.name, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000802
803
804 def remove_hosts(self, hosts):
Prathmesh Prabhuef7e5b02017-11-08 11:57:47 -0800805 # We must use the label's name instead of the id because label ids are
806 # not consistent across master-shard.
807 return self.afe.run('label_remove_hosts', id=self.name, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000808
809
mbligh5280e3b2008-12-22 14:39:28 +0000810class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000811 """
812 AFE acl object
813
814 Fields:
815 users, hosts, description, name, id
816 """
817 def __repr__(self):
818 return 'ACL: %s' % self.name
819
820
821 def add_hosts(self, hosts):
822 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
823 return self.afe.run('acl_group_add_hosts', self.id, hosts)
824
825
826 def remove_hosts(self, hosts):
827 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
828 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
829
830
mbligh54459c72009-01-21 19:26:44 +0000831 def add_users(self, users):
832 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
833 return self.afe.run('acl_group_add_users', id=self.name, users=users)
834
835
mbligh5280e3b2008-12-22 14:39:28 +0000836class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000837 """
838 AFE job object
839
840 Fields:
841 name, control_file, control_type, synch_count, reboot_before,
842 run_verify, priority, email_list, created_on, dependencies,
843 timeout, owner, reboot_after, id
844 """
845 def __repr__(self):
846 return 'JOB: %s' % self.id
847
848
mbligh5280e3b2008-12-22 14:39:28 +0000849class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000850 """
851 AFE job_status object
852
853 Fields:
854 status, complete, deleted, meta_host, host, active, execution_subdir, id
855 """
856 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800857 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000858 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700859 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000860 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000861
862
863 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000864 if self.host and self.host.hostname:
865 hostname = self.host.hostname
866 else:
867 hostname = 'None'
868 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000869
870
MK Ryu1b2d7f92015-02-24 17:45:02 -0800871class SpecialTask(RpcObject):
872 """
873 AFE special task object
874 """
875 def __init__(self, afe, hash):
876 super(SpecialTask, self).__init__(afe, hash)
877 self.host = Host(afe, self.host)
878
879
880 def __repr__(self):
881 return 'SPECIAL TASK: %s' % self.id
882
883
mbligh5280e3b2008-12-22 14:39:28 +0000884class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000885 """
886 AFE host object
887
888 Fields:
889 status, lock_time, locked_by, locked, hostname, invalid,
Allen Lie4c08272017-02-01 16:40:53 -0800890 labels, platform, protection, dirty, id
mbligh67647152008-11-19 00:18:14 +0000891 """
892 def __repr__(self):
893 return 'HOST OBJECT: %s' % self.hostname
894
895
896 def show(self):
897 labels = list(set(self.labels) - set([self.platform]))
898 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
899 self.locked, self.platform,
900 ', '.join(labels))
901
902
mbligh54459c72009-01-21 19:26:44 +0000903 def delete(self):
904 return self.afe.run('delete_host', id=self.id)
905
906
mbligh6463c4b2009-01-30 00:33:37 +0000907 def modify(self, **dargs):
908 return self.afe.run('modify_host', id=self.id, **dargs)
909
910
mbligh67647152008-11-19 00:18:14 +0000911 def get_acls(self):
912 return self.afe.get_acls(hosts__hostname=self.hostname)
913
914
915 def add_acl(self, acl_name):
916 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
917 return self.afe.run('acl_group_add_hosts', id=acl_name,
918 hosts=[self.hostname])
919
920
921 def remove_acl(self, acl_name):
922 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
923 return self.afe.run('acl_group_remove_hosts', id=acl_name,
924 hosts=[self.hostname])
925
926
927 def get_labels(self):
928 return self.afe.get_labels(host__hostname__in=[self.hostname])
929
930
931 def add_labels(self, labels):
932 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
933 return self.afe.run('host_add_labels', id=self.id, labels=labels)
934
935
936 def remove_labels(self, labels):
937 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
938 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000939
940
Allen Lif74957d2017-11-20 17:46:48 -0800941 def is_available(self):
942 """Check whether DUT host is available.
943
944 @return: bool
945 """
946 return not (self.locked
947 or self.status in host_states.UNAVAILABLE_STATES)
948
949
mbligh54459c72009-01-21 19:26:44 +0000950class User(RpcObject):
951 def __repr__(self):
952 return 'USER: %s' % self.login
953
954
mbligh5280e3b2008-12-22 14:39:28 +0000955class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000956 """
957 TKO test status object
958
959 Fields:
960 test_idx, hostname, testname, id
961 complete_count, incomplete_count, group_count, pass_count
962 """
963 def __repr__(self):
964 return 'TEST STATUS: %s' % self.id
965
966
MK Ryuacf35922014-10-03 14:56:49 -0700967class HostAttribute(RpcObject):
968 """
969 AFE host attribute object
970
971 Fields:
972 id, host, attribute, value
973 """
974 def __repr__(self):
975 return 'HOST ATTRIBUTE %d' % self.id