blob: fe942e010a354a026eb05ba8b5db7ed85beb0842 [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 Barnette81175432018-07-06 17:21:59 -0700203 def __init__(self, afe):
Richard Barnette260cbd02016-10-06 12:23:28 -0700204 self._afe = afe
Richard Barnette260cbd02016-10-06 12:23:28 -0700205
206
207 def get_all_versions(self):
208 """
209 Get all mappings in the stable versions table.
210
211 Extracts the full content of the `stable_version` table
212 in the AFE database, and returns it as a dictionary
213 mapping board names to version strings.
214
215 @return A dictionary mapping board names to version strings.
216 """
217 return self._afe.run('get_all_stable_versions')
218
219
220 def get_version(self, board):
221 """
222 Get the mapping of one board in the stable versions table.
223
224 Look up and return the version mapped to the given board in the
225 `stable_versions` table in the AFE database.
226
227 @param board The board to be looked up.
228
229 @return The version mapped for the given board.
230 """
Richard Barnette81175432018-07-06 17:21:59 -0700231 return self._afe.run('get_stable_version', board=board)
Richard Barnette260cbd02016-10-06 12:23:28 -0700232
233
234 def set_version(self, board, version):
235 """
236 Change the mapping of one board in the stable versions table.
237
238 Set the mapping in the `stable_versions` table in the AFE
239 database for the given board to the given version.
240
241 @param board The board to be updated.
242 @param version The new version to be assigned to the board.
243 """
Gregory Nisbet7f410662019-11-19 09:04:20 -0800244 raise RuntimeError("server.frontend._StableVersionMap::set_version is intentionally deleted")
Richard Barnette260cbd02016-10-06 12:23:28 -0700245
246
247 def delete_version(self, board):
248 """
249 Remove the mapping of one board in the stable versions table.
250
251 Remove the mapping in the `stable_versions` table in the AFE
252 database for the given board.
253
254 @param board The board to be updated.
255 """
Gregory Nisbet7f410662019-11-19 09:04:20 -0800256 raise RuntimeError("server.frontend._StableVersionMap::delete_version is intentionally deleted")
Richard Barnette260cbd02016-10-06 12:23:28 -0700257
258
259class _OSVersionMap(_StableVersionMap):
260 """
261 Abstract stable version mapping for full OS images of various types.
262 """
263
Richard Barnette206b25f2018-04-03 13:53:22 -0700264 def _version_is_valid(self, version):
265 return True
266
Richard Barnette260cbd02016-10-06 12:23:28 -0700267 def get_all_versions(self):
Richard Barnette260cbd02016-10-06 12:23:28 -0700268 versions = super(_OSVersionMap, self).get_all_versions()
269 for board in versions.keys():
Richard Barnette206b25f2018-04-03 13:53:22 -0700270 if ('/' in board
271 or not self._version_is_valid(versions[board])):
Richard Barnette260cbd02016-10-06 12:23:28 -0700272 del versions[board]
273 return versions
274
Richard Barnette206b25f2018-04-03 13:53:22 -0700275 def get_version(self, board):
276 version = super(_OSVersionMap, self).get_version(board)
277 return version if self._version_is_valid(version) else None
278
Richard Barnette260cbd02016-10-06 12:23:28 -0700279
Richard Barnette08e487d2018-03-30 18:39:31 -0700280def format_cros_image_name(board, version):
281 """
282 Return an image name for a given `board` and `version`.
283
284 This formats `board` and `version` into a string identifying an
285 image file. The string represents part of a URL for access to
286 the image.
287
288 The returned image name is typically of a form like
289 "falco-release/R55-8872.44.0".
290 """
291 build_pattern = GLOBAL_CONFIG.get_config_value(
292 'CROS', 'stable_build_pattern')
293 return build_pattern % (board, version)
294
295
Richard Barnette260cbd02016-10-06 12:23:28 -0700296class _CrosVersionMap(_OSVersionMap):
297 """
298 Stable version mapping for Chrome OS release images.
299
300 This class manages a mapping of Chrome OS board names to known-good
301 release (or canary) images. The images selected can be installed on
302 DUTs during repair tasks, as a way of getting a DUT into a known
303 working state.
304 """
305
Richard Barnette206b25f2018-04-03 13:53:22 -0700306 def _version_is_valid(self, version):
307 return version is not None and '/' not in version
308
Richard Barnette383ef9c2016-12-13 11:56:49 -0800309 def get_image_name(self, board):
310 """
311 Return the full image name of the stable version for `board`.
312
313 This finds the stable version for `board`, and returns a string
Richard Barnette728e36f2016-11-03 16:04:29 -0700314 identifying the associated image as for `format_image_name()`,
315 above.
Richard Barnette383ef9c2016-12-13 11:56:49 -0800316
317 @return A string identifying the image file for the stable
318 image for `board`.
319 """
Richard Barnette08e487d2018-03-30 18:39:31 -0700320 return format_cros_image_name(board, self.get_version(board))
Richard Barnette260cbd02016-10-06 12:23:28 -0700321
322
Richard Barnettee50453e2016-10-10 16:43:44 -0700323class _SuffixHackVersionMap(_StableVersionMap):
Richard Barnette260cbd02016-10-06 12:23:28 -0700324 """
Richard Barnettee50453e2016-10-10 16:43:44 -0700325 Abstract super class for mappings using a pseudo-board name.
Richard Barnette260cbd02016-10-06 12:23:28 -0700326
Richard Barnettee50453e2016-10-10 16:43:44 -0700327 For non-OS image type mappings, we look them up in the
328 `stable_versions` table by constructing a "pseudo-board" from the
329 real board name plus a suffix string that identifies the image type.
330 So, for instance the name "lulu/firmware" is used to look up the
331 FAFT firmware version for lulu boards.
Richard Barnette260cbd02016-10-06 12:23:28 -0700332 """
333
Richard Barnettee50453e2016-10-10 16:43:44 -0700334 # _SUFFIX - The suffix used in constructing the "pseudo-board"
335 # lookup key. Each subclass must define this value for itself.
Richard Barnette260cbd02016-10-06 12:23:28 -0700336 #
Richard Barnettee50453e2016-10-10 16:43:44 -0700337 _SUFFIX = None
Richard Barnette260cbd02016-10-06 12:23:28 -0700338
Richard Barnette260cbd02016-10-06 12:23:28 -0700339 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700340 # Get all the mappings from the AFE, extract just the mappings
341 # with our suffix, and replace the pseudo-board name keys with
Richard Barnette260cbd02016-10-06 12:23:28 -0700342 # the real board names.
343 #
344 all_versions = super(
Richard Barnettee50453e2016-10-10 16:43:44 -0700345 _SuffixHackVersionMap, self).get_all_versions()
Richard Barnette260cbd02016-10-06 12:23:28 -0700346 return {
347 board[0 : -len(self._SUFFIX)]: all_versions[board]
348 for board in all_versions.keys()
349 if board.endswith(self._SUFFIX)
350 }
351
352
353 def get_version(self, board):
Richard Barnettee50453e2016-10-10 16:43:44 -0700354 board += self._SUFFIX
355 return super(_SuffixHackVersionMap, self).get_version(board)
356
357
358 def set_version(self, board, version):
359 board += self._SUFFIX
360 super(_SuffixHackVersionMap, self).set_version(board, version)
361
362
363 def delete_version(self, board):
364 board += self._SUFFIX
365 super(_SuffixHackVersionMap, self).delete_version(board)
366
367
368class _FAFTVersionMap(_SuffixHackVersionMap):
369 """
370 Stable version mapping for firmware versions used in FAFT repair.
371
372 When DUTs used for FAFT fail repair, stable firmware may need to be
373 flashed directly from original tarballs. The FAFT firmware version
374 mapping finds the appropriate tarball for a given board.
375 """
376
377 _SUFFIX = '/firmware'
378
379 def get_version(self, board):
380 # If there's no mapping for `board`, the lookup will return the
381 # default CrOS version mapping. To eliminate that case, we
382 # require a '/' character in the version, since CrOS versions
383 # won't match that.
Richard Barnette260cbd02016-10-06 12:23:28 -0700384 #
385 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
386 # the right fix is to move handling to the RPC server side.
387 #
Richard Barnette260cbd02016-10-06 12:23:28 -0700388 version = super(_FAFTVersionMap, self).get_version(board)
389 return version if '/' in version else None
390
391
Richard Barnettee50453e2016-10-10 16:43:44 -0700392class _FirmwareVersionMap(_SuffixHackVersionMap):
393 """
394 Stable version mapping for firmware supplied in Chrome OS images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700395
Richard Barnettee50453e2016-10-10 16:43:44 -0700396 A Chrome OS image bundles a version of the firmware that the
397 device should update to when the OS version is installed during
398 AU.
Richard Barnette260cbd02016-10-06 12:23:28 -0700399
Richard Barnettee50453e2016-10-10 16:43:44 -0700400 Test images suppress the firmware update during AU. Instead, during
401 repair and verify we check installed firmware on a DUT, compare it
402 against the stable version mapping for the board, and update when
403 the DUT is out-of-date.
404 """
405
406 _SUFFIX = '/rwfw'
407
408 def get_version(self, board):
409 # If there's no mapping for `board`, the lookup will return the
410 # default CrOS version mapping. To eliminate that case, we
411 # require the version start with "Google_", since CrOS versions
412 # won't match that.
413 #
414 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
415 # the right fix is to move handling to the RPC server side.
416 #
417 version = super(_FirmwareVersionMap, self).get_version(board)
418 return version if version.startswith('Google_') else None
Richard Barnette260cbd02016-10-06 12:23:28 -0700419
420
mbligh5280e3b2008-12-22 14:39:28 +0000421class AFE(RpcClient):
mbligh1ef218d2009-08-03 16:57:56 +0000422
Richard Barnette260cbd02016-10-06 12:23:28 -0700423 # Known image types for stable version mapping objects.
424 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
425 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
Richard Barnettee50453e2016-10-10 16:43:44 -0700426 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700427 #
428 CROS_IMAGE_TYPE = 'cros'
429 FAFT_IMAGE_TYPE = 'faft'
Richard Barnettee50453e2016-10-10 16:43:44 -0700430 FIRMWARE_IMAGE_TYPE = 'firmware'
Richard Barnette260cbd02016-10-06 12:23:28 -0700431
432 _IMAGE_MAPPING_CLASSES = {
433 CROS_IMAGE_TYPE: _CrosVersionMap,
434 FAFT_IMAGE_TYPE: _FAFTVersionMap,
Richard Barnettee50453e2016-10-10 16:43:44 -0700435 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
Richard Barnette260cbd02016-10-06 12:23:28 -0700436 }
437
438
Prathmesh Prabhu6d5ba592017-01-05 13:56:04 -0800439 def __init__(self, user=None, server=None, print_log=True, debug=False,
440 reply_debug=False, job=None):
441 self.job = job
442 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
443 user=user,
444 server=server,
445 print_log=print_log,
446 debug=debug,
447 reply_debug=reply_debug)
448
449
Richard Barnette260cbd02016-10-06 12:23:28 -0700450 def get_stable_version_map(self, image_type):
451 """
452 Return a stable version mapping for the given image type.
453
454 @return An object mapping board names to version strings for
455 software of the given image type.
456 """
457 return self._IMAGE_MAPPING_CLASSES[image_type](self)
458
459
mbligh67647152008-11-19 00:18:14 +0000460 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000461 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000462 statuses = self.run('get_static_data')['host_statuses']
463 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000464 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000465 if live == False:
466 return dead_statuses
467 else:
468 return statuses
469
470
mbligh71094012009-12-19 05:35:21 +0000471 @staticmethod
472 def _dict_for_host_query(hostnames=(), status=None, label=None):
473 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000474 if hostnames:
475 query_args['hostname__in'] = hostnames
476 if status:
477 query_args['status'] = status
478 if label:
479 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000480 return query_args
481
482
483 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
484 query_args = dict(dargs)
485 query_args.update(self._dict_for_host_query(hostnames=hostnames,
486 status=status,
487 label=label))
488 hosts = self.run('get_hosts', **query_args)
489 return [Host(self, h) for h in hosts]
490
491
492 def get_hostnames(self, status=None, label=None, **dargs):
493 """Like get_hosts() but returns hostnames instead of Host objects."""
494 # This implementation can be replaced with a more efficient one
495 # that does not query for entire host objects in the future.
496 return [host_obj.hostname for host_obj in
497 self.get_hosts(status=status, label=label, **dargs)]
498
499
500 def reverify_hosts(self, hostnames=(), status=None, label=None):
501 query_args = dict(locked=False,
502 aclgroup__users__login=self.user)
503 query_args.update(self._dict_for_host_query(hostnames=hostnames,
504 status=status,
505 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000506 return self.run('reverify_hosts', **query_args)
507
508
Richard Barnette73e74b02017-08-10 15:16:49 -0700509 def repair_hosts(self, hostnames=(), status=None, label=None):
510 query_args = dict(locked=False,
511 aclgroup__users__login=self.user)
512 query_args.update(self._dict_for_host_query(hostnames=hostnames,
513 status=status,
514 label=label))
515 return self.run('repair_hosts', **query_args)
516
517
mbligh67647152008-11-19 00:18:14 +0000518 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000519 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000520 return self.get_hosts(id=id)[0]
521
522
MK Ryuacf35922014-10-03 14:56:49 -0700523 def get_host_attribute(self, attr, **dargs):
524 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
525 return [HostAttribute(self, a) for a in host_attrs]
526
527
Chris Masone8abb6fc2012-01-31 09:27:36 -0800528 def set_host_attribute(self, attr, val, **dargs):
529 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
530
531
mbligh67647152008-11-19 00:18:14 +0000532 def get_labels(self, **dargs):
533 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000534 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000535
536
537 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000538 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000539 return self.get_labels(id=id)[0]
540
541
542 def get_acls(self, **dargs):
543 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000544 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000545
546
547 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000548 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000549 return self.get_acls(id=id)[0]
550
551
mbligh54459c72009-01-21 19:26:44 +0000552 def get_users(self, **dargs):
553 users = self.run('get_users', **dargs)
554 return [User(self, u) for u in users]
555
556
mbligh1354c9d2008-12-22 14:56:13 +0000557 def generate_control_file(self, tests, **dargs):
558 ret = self.run('generate_control_file', tests=tests, **dargs)
559 return ControlFile(self, ret)
560
561
mbligh67647152008-11-19 00:18:14 +0000562 def get_jobs(self, summary=False, **dargs):
563 if summary:
564 jobs_data = self.run('get_jobs_summary', **dargs)
565 else:
566 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000567 jobs = []
568 for j in jobs_data:
569 job = Job(self, j)
570 # Set up some extra information defaults
571 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
572 job.platform_results = {}
573 job.platform_reasons = {}
574 jobs.append(job)
575 return jobs
mbligh67647152008-11-19 00:18:14 +0000576
577
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700578 def get_host_queue_entries(self, **kwargs):
579 """Find JobStatus objects matching some constraints.
mbligh99b24f42009-06-08 16:45:55 +0000580
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700581 @param **kwargs: Arguments to pass to the RPC
582 """
583 entries = self.run('get_host_queue_entries', **kwargs)
584 return self._entries_to_statuses(entries)
585
586
587 def get_host_queue_entries_by_insert_time(self, **kwargs):
588 """Like get_host_queue_entries, but using the insert index table.
589
590 @param **kwargs: Arguments to pass to the RPC
591 """
592 entries = self.run('get_host_queue_entries_by_insert_time', **kwargs)
593 return self._entries_to_statuses(entries)
594
595
596 def _entries_to_statuses(self, entries):
597 """Converts HQEs to JobStatuses
598
599 Sadly, get_host_queue_entries doesn't return platforms, we have
600 to get those back from an explicit get_hosts queury, then patch
601 the new host objects back into the host list.
602
603 :param entries: A list of HQEs from get_host_queue_entries or
604 get_host_queue_entries_by_insert_time.
605 """
606 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000607 hostnames = [s.host.hostname for s in job_statuses if s.host]
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700608 hosts = {}
mbligh99b24f42009-06-08 16:45:55 +0000609 for host in self.get_hosts(hostname__in=hostnames):
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700610 hosts[host.hostname] = host
mbligh99b24f42009-06-08 16:45:55 +0000611 for status in job_statuses:
612 if status.host:
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700613 status.host = hosts.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000614 # filter job statuses that have either host or meta_host
615 return [status for status in job_statuses if (status.host or
616 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000617
618
MK Ryu1b2d7f92015-02-24 17:45:02 -0800619 def get_special_tasks(self, **data):
620 tasks = self.run('get_special_tasks', **data)
621 return [SpecialTask(self, t) for t in tasks]
622
623
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700624 def get_host_special_tasks(self, host_id, **data):
625 tasks = self.run('get_host_special_tasks',
626 host_id=host_id, **data)
627 return [SpecialTask(self, t) for t in tasks]
628
629
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700630 def get_host_status_task(self, host_id, end_time):
631 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700632 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700633 return SpecialTask(self, task) if task else None
634
635
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700636 def get_host_diagnosis_interval(self, host_id, end_time, success):
637 return self.run('get_host_diagnosis_interval',
638 host_id=host_id, end_time=end_time,
639 success=success)
640
641
Allen Li352b86a2016-12-14 12:11:27 -0800642 def create_job(self, control_file, name=' ',
643 priority=priorities.Priority.DEFAULT,
644 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT,
645 **dargs):
mbligh67647152008-11-19 00:18:14 +0000646 id = self.run('create_job', name=name, priority=priority,
647 control_file=control_file, control_type=control_type, **dargs)
648 return self.get_jobs(id=id)[0]
649
650
Simran Basi01984f52015-10-12 15:36:45 -0700651 def abort_jobs(self, jobs):
652 """Abort a list of jobs.
653
654 Already completed jobs will not be affected.
655
656 @param jobs: List of job ids to abort.
657 """
658 for job in jobs:
659 self.run('abort_host_queue_entries', job_id=job)
660
661
Kevin Cheng19521982016-09-22 12:27:23 -0700662 def get_hosts_by_attribute(self, attribute, value):
663 """
664 Get the list of hosts that share the same host attribute value.
665
666 @param attribute: String of the host attribute to check.
667 @param value: String of the value that is shared between hosts.
668
669 @returns List of hostnames that all have the same host attribute and
670 value.
671 """
672 return self.run('get_hosts_by_attribute',
673 attribute=attribute, value=value)
674
675
676 def lock_host(self, host, lock_reason, fail_if_locked=False):
677 """
678 Lock the given host with the given lock reason.
679
680 Locking a host that's already locked using the 'modify_hosts' rpc
681 will raise an exception. That's why fail_if_locked exists so the
682 caller can determine if the lock succeeded or failed. This will
683 save every caller from wrapping lock_host in a try-except.
684
685 @param host: hostname of host to lock.
686 @param lock_reason: Reason for locking host.
687 @param fail_if_locked: Return False if host is already locked.
688
689 @returns Boolean, True if lock was successful, False otherwise.
690 """
691 try:
692 self.run('modify_hosts',
693 host_filter_data={'hostname': host},
694 update_data={'locked': True,
695 'lock_reason': lock_reason})
696 except Exception:
697 return not fail_if_locked
698 return True
699
700
701 def unlock_hosts(self, locked_hosts):
702 """
703 Unlock the hosts.
704
705 Unlocking a host that's already unlocked will do nothing so we don't
706 need any special try-except clause here.
707
708 @param locked_hosts: List of hostnames of hosts to unlock.
709 """
710 self.run('modify_hosts',
711 host_filter_data={'hostname__in': locked_hosts},
712 update_data={'locked': False,
713 'lock_reason': ''})
714
715
mbligh5280e3b2008-12-22 14:39:28 +0000716class TestResults(object):
717 """
718 Container class used to hold the results of the tests for a job
719 """
720 def __init__(self):
721 self.good = []
722 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000723 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000724
725
726 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000727 if result.complete_count > result.pass_count:
728 self.fail.append(result)
729 elif result.incomplete_count > 0:
730 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000731 else:
mbligh451ede12009-02-12 21:54:03 +0000732 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000733
734
735class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000736 """
737 Generic object used to construct python objects from rpc calls
738 """
739 def __init__(self, afe, hash):
740 self.afe = afe
741 self.hash = hash
742 self.__dict__.update(hash)
743
744
745 def __str__(self):
746 return dump_object(self.__repr__(), self)
747
748
mbligh1354c9d2008-12-22 14:56:13 +0000749class ControlFile(RpcObject):
750 """
751 AFE control file object
752
753 Fields: synch_count, dependencies, control_file, is_server
754 """
755 def __repr__(self):
756 return 'CONTROL FILE: %s' % self.control_file
757
758
mbligh5280e3b2008-12-22 14:39:28 +0000759class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000760 """
761 AFE label object
762
763 Fields:
764 name, invalid, platform, kernel_config, id, only_if_needed
765 """
766 def __repr__(self):
767 return 'LABEL: %s' % self.name
768
769
770 def add_hosts(self, hosts):
Prathmesh Prabhuef7e5b02017-11-08 11:57:47 -0800771 # We must use the label's name instead of the id because label ids are
772 # not consistent across master-shard.
773 return self.afe.run('label_add_hosts', id=self.name, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000774
775
776 def remove_hosts(self, hosts):
Prathmesh Prabhuef7e5b02017-11-08 11:57:47 -0800777 # We must use the label's name instead of the id because label ids are
778 # not consistent across master-shard.
779 return self.afe.run('label_remove_hosts', id=self.name, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000780
781
mbligh5280e3b2008-12-22 14:39:28 +0000782class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000783 """
784 AFE acl object
785
786 Fields:
787 users, hosts, description, name, id
788 """
789 def __repr__(self):
790 return 'ACL: %s' % self.name
791
792
793 def add_hosts(self, hosts):
794 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
795 return self.afe.run('acl_group_add_hosts', self.id, hosts)
796
797
798 def remove_hosts(self, hosts):
799 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
800 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
801
802
mbligh54459c72009-01-21 19:26:44 +0000803 def add_users(self, users):
804 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
805 return self.afe.run('acl_group_add_users', id=self.name, users=users)
806
807
mbligh5280e3b2008-12-22 14:39:28 +0000808class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000809 """
810 AFE job object
811
812 Fields:
813 name, control_file, control_type, synch_count, reboot_before,
814 run_verify, priority, email_list, created_on, dependencies,
815 timeout, owner, reboot_after, id
816 """
817 def __repr__(self):
818 return 'JOB: %s' % self.id
819
820
mbligh5280e3b2008-12-22 14:39:28 +0000821class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000822 """
823 AFE job_status object
824
825 Fields:
826 status, complete, deleted, meta_host, host, active, execution_subdir, id
827 """
828 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800829 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000830 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700831 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000832 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000833
834
835 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000836 if self.host and self.host.hostname:
837 hostname = self.host.hostname
838 else:
839 hostname = 'None'
840 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000841
842
MK Ryu1b2d7f92015-02-24 17:45:02 -0800843class SpecialTask(RpcObject):
844 """
845 AFE special task object
846 """
847 def __init__(self, afe, hash):
848 super(SpecialTask, self).__init__(afe, hash)
849 self.host = Host(afe, self.host)
850
851
852 def __repr__(self):
853 return 'SPECIAL TASK: %s' % self.id
854
855
mbligh5280e3b2008-12-22 14:39:28 +0000856class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000857 """
858 AFE host object
859
860 Fields:
861 status, lock_time, locked_by, locked, hostname, invalid,
Allen Lie4c08272017-02-01 16:40:53 -0800862 labels, platform, protection, dirty, id
mbligh67647152008-11-19 00:18:14 +0000863 """
864 def __repr__(self):
865 return 'HOST OBJECT: %s' % self.hostname
866
867
868 def show(self):
869 labels = list(set(self.labels) - set([self.platform]))
870 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
871 self.locked, self.platform,
872 ', '.join(labels))
873
874
mbligh54459c72009-01-21 19:26:44 +0000875 def delete(self):
876 return self.afe.run('delete_host', id=self.id)
877
878
mbligh6463c4b2009-01-30 00:33:37 +0000879 def modify(self, **dargs):
880 return self.afe.run('modify_host', id=self.id, **dargs)
881
882
mbligh67647152008-11-19 00:18:14 +0000883 def get_acls(self):
884 return self.afe.get_acls(hosts__hostname=self.hostname)
885
886
887 def add_acl(self, acl_name):
888 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
889 return self.afe.run('acl_group_add_hosts', id=acl_name,
890 hosts=[self.hostname])
891
892
893 def remove_acl(self, acl_name):
894 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
895 return self.afe.run('acl_group_remove_hosts', id=acl_name,
896 hosts=[self.hostname])
897
898
899 def get_labels(self):
900 return self.afe.get_labels(host__hostname__in=[self.hostname])
901
902
903 def add_labels(self, labels):
904 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
905 return self.afe.run('host_add_labels', id=self.id, labels=labels)
906
907
908 def remove_labels(self, labels):
909 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
910 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000911
912
Allen Lif74957d2017-11-20 17:46:48 -0800913 def is_available(self):
914 """Check whether DUT host is available.
915
916 @return: bool
917 """
918 return not (self.locked
919 or self.status in host_states.UNAVAILABLE_STATES)
920
921
mbligh54459c72009-01-21 19:26:44 +0000922class User(RpcObject):
923 def __repr__(self):
924 return 'USER: %s' % self.login
925
926
mbligh5280e3b2008-12-22 14:39:28 +0000927class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000928 """
929 TKO test status object
930
931 Fields:
932 test_idx, hostname, testname, id
933 complete_count, incomplete_count, group_count, pass_count
934 """
935 def __repr__(self):
936 return 'TEST STATUS: %s' % self.id
937
938
MK Ryuacf35922014-10-03 14:56:49 -0700939class HostAttribute(RpcObject):
940 """
941 AFE host attribute object
942
943 Fields:
944 id, host, attribute, value
945 """
946 def __repr__(self):
947 return 'HOST ATTRIBUTE %d' % self.id