blob: 5f36cad52b3405f852224e9b7dad9d73826f6084 [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 """
244 self._afe.run('set_stable_version',
245 version=version, board=board)
246
247
248 def delete_version(self, board):
249 """
250 Remove the mapping of one board in the stable versions table.
251
252 Remove the mapping in the `stable_versions` table in the AFE
253 database for the given board.
254
255 @param board The board to be updated.
256 """
257 self._afe.run('delete_stable_version', board=board)
258
259
260class _OSVersionMap(_StableVersionMap):
261 """
262 Abstract stable version mapping for full OS images of various types.
263 """
264
Richard Barnette206b25f2018-04-03 13:53:22 -0700265 def _version_is_valid(self, version):
266 return True
267
Richard Barnette260cbd02016-10-06 12:23:28 -0700268 def get_all_versions(self):
Richard Barnette260cbd02016-10-06 12:23:28 -0700269 versions = super(_OSVersionMap, self).get_all_versions()
270 for board in versions.keys():
Richard Barnette206b25f2018-04-03 13:53:22 -0700271 if ('/' in board
272 or not self._version_is_valid(versions[board])):
Richard Barnette260cbd02016-10-06 12:23:28 -0700273 del versions[board]
274 return versions
275
Richard Barnette206b25f2018-04-03 13:53:22 -0700276 def get_version(self, board):
277 version = super(_OSVersionMap, self).get_version(board)
278 return version if self._version_is_valid(version) else None
279
Richard Barnette260cbd02016-10-06 12:23:28 -0700280
Richard Barnette08e487d2018-03-30 18:39:31 -0700281def format_cros_image_name(board, version):
282 """
283 Return an image name for a given `board` and `version`.
284
285 This formats `board` and `version` into a string identifying an
286 image file. The string represents part of a URL for access to
287 the image.
288
289 The returned image name is typically of a form like
290 "falco-release/R55-8872.44.0".
291 """
292 build_pattern = GLOBAL_CONFIG.get_config_value(
293 'CROS', 'stable_build_pattern')
294 return build_pattern % (board, version)
295
296
Richard Barnette260cbd02016-10-06 12:23:28 -0700297class _CrosVersionMap(_OSVersionMap):
298 """
299 Stable version mapping for Chrome OS release images.
300
301 This class manages a mapping of Chrome OS board names to known-good
302 release (or canary) images. The images selected can be installed on
303 DUTs during repair tasks, as a way of getting a DUT into a known
304 working state.
305 """
306
Richard Barnette206b25f2018-04-03 13:53:22 -0700307 def _version_is_valid(self, version):
308 return version is not None and '/' not in version
309
Richard Barnette383ef9c2016-12-13 11:56:49 -0800310 def get_image_name(self, board):
311 """
312 Return the full image name of the stable version for `board`.
313
314 This finds the stable version for `board`, and returns a string
Richard Barnette728e36f2016-11-03 16:04:29 -0700315 identifying the associated image as for `format_image_name()`,
316 above.
Richard Barnette383ef9c2016-12-13 11:56:49 -0800317
318 @return A string identifying the image file for the stable
319 image for `board`.
320 """
Richard Barnette08e487d2018-03-30 18:39:31 -0700321 return format_cros_image_name(board, self.get_version(board))
Richard Barnette260cbd02016-10-06 12:23:28 -0700322
323
Richard Barnettee50453e2016-10-10 16:43:44 -0700324class _SuffixHackVersionMap(_StableVersionMap):
Richard Barnette260cbd02016-10-06 12:23:28 -0700325 """
Richard Barnettee50453e2016-10-10 16:43:44 -0700326 Abstract super class for mappings using a pseudo-board name.
Richard Barnette260cbd02016-10-06 12:23:28 -0700327
Richard Barnettee50453e2016-10-10 16:43:44 -0700328 For non-OS image type mappings, we look them up in the
329 `stable_versions` table by constructing a "pseudo-board" from the
330 real board name plus a suffix string that identifies the image type.
331 So, for instance the name "lulu/firmware" is used to look up the
332 FAFT firmware version for lulu boards.
Richard Barnette260cbd02016-10-06 12:23:28 -0700333 """
334
Richard Barnettee50453e2016-10-10 16:43:44 -0700335 # _SUFFIX - The suffix used in constructing the "pseudo-board"
336 # lookup key. Each subclass must define this value for itself.
Richard Barnette260cbd02016-10-06 12:23:28 -0700337 #
Richard Barnettee50453e2016-10-10 16:43:44 -0700338 _SUFFIX = None
Richard Barnette260cbd02016-10-06 12:23:28 -0700339
Richard Barnette260cbd02016-10-06 12:23:28 -0700340 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700341 # Get all the mappings from the AFE, extract just the mappings
342 # with our suffix, and replace the pseudo-board name keys with
Richard Barnette260cbd02016-10-06 12:23:28 -0700343 # the real board names.
344 #
345 all_versions = super(
Richard Barnettee50453e2016-10-10 16:43:44 -0700346 _SuffixHackVersionMap, self).get_all_versions()
Richard Barnette260cbd02016-10-06 12:23:28 -0700347 return {
348 board[0 : -len(self._SUFFIX)]: all_versions[board]
349 for board in all_versions.keys()
350 if board.endswith(self._SUFFIX)
351 }
352
353
354 def get_version(self, board):
Richard Barnettee50453e2016-10-10 16:43:44 -0700355 board += self._SUFFIX
356 return super(_SuffixHackVersionMap, self).get_version(board)
357
358
359 def set_version(self, board, version):
360 board += self._SUFFIX
361 super(_SuffixHackVersionMap, self).set_version(board, version)
362
363
364 def delete_version(self, board):
365 board += self._SUFFIX
366 super(_SuffixHackVersionMap, self).delete_version(board)
367
368
369class _FAFTVersionMap(_SuffixHackVersionMap):
370 """
371 Stable version mapping for firmware versions used in FAFT repair.
372
373 When DUTs used for FAFT fail repair, stable firmware may need to be
374 flashed directly from original tarballs. The FAFT firmware version
375 mapping finds the appropriate tarball for a given board.
376 """
377
378 _SUFFIX = '/firmware'
379
380 def get_version(self, board):
381 # If there's no mapping for `board`, the lookup will return the
382 # default CrOS version mapping. To eliminate that case, we
383 # require a '/' character in the version, since CrOS versions
384 # won't match that.
Richard Barnette260cbd02016-10-06 12:23:28 -0700385 #
386 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
387 # the right fix is to move handling to the RPC server side.
388 #
Richard Barnette260cbd02016-10-06 12:23:28 -0700389 version = super(_FAFTVersionMap, self).get_version(board)
390 return version if '/' in version else None
391
392
Richard Barnettee50453e2016-10-10 16:43:44 -0700393class _FirmwareVersionMap(_SuffixHackVersionMap):
394 """
395 Stable version mapping for firmware supplied in Chrome OS images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700396
Richard Barnettee50453e2016-10-10 16:43:44 -0700397 A Chrome OS image bundles a version of the firmware that the
398 device should update to when the OS version is installed during
399 AU.
Richard Barnette260cbd02016-10-06 12:23:28 -0700400
Richard Barnettee50453e2016-10-10 16:43:44 -0700401 Test images suppress the firmware update during AU. Instead, during
402 repair and verify we check installed firmware on a DUT, compare it
403 against the stable version mapping for the board, and update when
404 the DUT is out-of-date.
405 """
406
407 _SUFFIX = '/rwfw'
408
409 def get_version(self, board):
410 # If there's no mapping for `board`, the lookup will return the
411 # default CrOS version mapping. To eliminate that case, we
412 # require the version start with "Google_", since CrOS versions
413 # won't match that.
414 #
415 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
416 # the right fix is to move handling to the RPC server side.
417 #
418 version = super(_FirmwareVersionMap, self).get_version(board)
419 return version if version.startswith('Google_') else None
Richard Barnette260cbd02016-10-06 12:23:28 -0700420
421
mbligh5280e3b2008-12-22 14:39:28 +0000422class AFE(RpcClient):
mbligh1ef218d2009-08-03 16:57:56 +0000423
Richard Barnette260cbd02016-10-06 12:23:28 -0700424 # Known image types for stable version mapping objects.
425 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
426 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
Richard Barnettee50453e2016-10-10 16:43:44 -0700427 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700428 #
429 CROS_IMAGE_TYPE = 'cros'
430 FAFT_IMAGE_TYPE = 'faft'
Richard Barnettee50453e2016-10-10 16:43:44 -0700431 FIRMWARE_IMAGE_TYPE = 'firmware'
Richard Barnette260cbd02016-10-06 12:23:28 -0700432
433 _IMAGE_MAPPING_CLASSES = {
434 CROS_IMAGE_TYPE: _CrosVersionMap,
435 FAFT_IMAGE_TYPE: _FAFTVersionMap,
Richard Barnettee50453e2016-10-10 16:43:44 -0700436 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
Richard Barnette260cbd02016-10-06 12:23:28 -0700437 }
438
439
Prathmesh Prabhu6d5ba592017-01-05 13:56:04 -0800440 def __init__(self, user=None, server=None, print_log=True, debug=False,
441 reply_debug=False, job=None):
442 self.job = job
443 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
444 user=user,
445 server=server,
446 print_log=print_log,
447 debug=debug,
448 reply_debug=reply_debug)
449
450
Richard Barnette260cbd02016-10-06 12:23:28 -0700451 def get_stable_version_map(self, image_type):
452 """
453 Return a stable version mapping for the given image type.
454
455 @return An object mapping board names to version strings for
456 software of the given image type.
457 """
458 return self._IMAGE_MAPPING_CLASSES[image_type](self)
459
460
mbligh67647152008-11-19 00:18:14 +0000461 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000462 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000463 statuses = self.run('get_static_data')['host_statuses']
464 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000465 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000466 if live == False:
467 return dead_statuses
468 else:
469 return statuses
470
471
mbligh71094012009-12-19 05:35:21 +0000472 @staticmethod
473 def _dict_for_host_query(hostnames=(), status=None, label=None):
474 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000475 if hostnames:
476 query_args['hostname__in'] = hostnames
477 if status:
478 query_args['status'] = status
479 if label:
480 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000481 return query_args
482
483
484 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
485 query_args = dict(dargs)
486 query_args.update(self._dict_for_host_query(hostnames=hostnames,
487 status=status,
488 label=label))
489 hosts = self.run('get_hosts', **query_args)
490 return [Host(self, h) for h in hosts]
491
492
493 def get_hostnames(self, status=None, label=None, **dargs):
494 """Like get_hosts() but returns hostnames instead of Host objects."""
495 # This implementation can be replaced with a more efficient one
496 # that does not query for entire host objects in the future.
497 return [host_obj.hostname for host_obj in
498 self.get_hosts(status=status, label=label, **dargs)]
499
500
501 def reverify_hosts(self, hostnames=(), status=None, label=None):
502 query_args = dict(locked=False,
503 aclgroup__users__login=self.user)
504 query_args.update(self._dict_for_host_query(hostnames=hostnames,
505 status=status,
506 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000507 return self.run('reverify_hosts', **query_args)
508
509
Richard Barnette73e74b02017-08-10 15:16:49 -0700510 def repair_hosts(self, hostnames=(), status=None, label=None):
511 query_args = dict(locked=False,
512 aclgroup__users__login=self.user)
513 query_args.update(self._dict_for_host_query(hostnames=hostnames,
514 status=status,
515 label=label))
516 return self.run('repair_hosts', **query_args)
517
518
mbligh67647152008-11-19 00:18:14 +0000519 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000520 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000521 return self.get_hosts(id=id)[0]
522
523
MK Ryuacf35922014-10-03 14:56:49 -0700524 def get_host_attribute(self, attr, **dargs):
525 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
526 return [HostAttribute(self, a) for a in host_attrs]
527
528
Chris Masone8abb6fc2012-01-31 09:27:36 -0800529 def set_host_attribute(self, attr, val, **dargs):
530 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
531
532
mbligh67647152008-11-19 00:18:14 +0000533 def get_labels(self, **dargs):
534 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000535 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000536
537
538 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000539 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000540 return self.get_labels(id=id)[0]
541
542
543 def get_acls(self, **dargs):
544 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000545 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000546
547
548 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000549 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000550 return self.get_acls(id=id)[0]
551
552
mbligh54459c72009-01-21 19:26:44 +0000553 def get_users(self, **dargs):
554 users = self.run('get_users', **dargs)
555 return [User(self, u) for u in users]
556
557
mbligh1354c9d2008-12-22 14:56:13 +0000558 def generate_control_file(self, tests, **dargs):
559 ret = self.run('generate_control_file', tests=tests, **dargs)
560 return ControlFile(self, ret)
561
562
mbligh67647152008-11-19 00:18:14 +0000563 def get_jobs(self, summary=False, **dargs):
564 if summary:
565 jobs_data = self.run('get_jobs_summary', **dargs)
566 else:
567 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000568 jobs = []
569 for j in jobs_data:
570 job = Job(self, j)
571 # Set up some extra information defaults
572 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
573 job.platform_results = {}
574 job.platform_reasons = {}
575 jobs.append(job)
576 return jobs
mbligh67647152008-11-19 00:18:14 +0000577
578
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700579 def get_host_queue_entries(self, **kwargs):
580 """Find JobStatus objects matching some constraints.
mbligh99b24f42009-06-08 16:45:55 +0000581
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700582 @param **kwargs: Arguments to pass to the RPC
583 """
584 entries = self.run('get_host_queue_entries', **kwargs)
585 return self._entries_to_statuses(entries)
586
587
588 def get_host_queue_entries_by_insert_time(self, **kwargs):
589 """Like get_host_queue_entries, but using the insert index table.
590
591 @param **kwargs: Arguments to pass to the RPC
592 """
593 entries = self.run('get_host_queue_entries_by_insert_time', **kwargs)
594 return self._entries_to_statuses(entries)
595
596
597 def _entries_to_statuses(self, entries):
598 """Converts HQEs to JobStatuses
599
600 Sadly, get_host_queue_entries doesn't return platforms, we have
601 to get those back from an explicit get_hosts queury, then patch
602 the new host objects back into the host list.
603
604 :param entries: A list of HQEs from get_host_queue_entries or
605 get_host_queue_entries_by_insert_time.
606 """
607 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000608 hostnames = [s.host.hostname for s in job_statuses if s.host]
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700609 hosts = {}
mbligh99b24f42009-06-08 16:45:55 +0000610 for host in self.get_hosts(hostname__in=hostnames):
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700611 hosts[host.hostname] = host
mbligh99b24f42009-06-08 16:45:55 +0000612 for status in job_statuses:
613 if status.host:
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700614 status.host = hosts.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000615 # filter job statuses that have either host or meta_host
616 return [status for status in job_statuses if (status.host or
617 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000618
619
MK Ryu1b2d7f92015-02-24 17:45:02 -0800620 def get_special_tasks(self, **data):
621 tasks = self.run('get_special_tasks', **data)
622 return [SpecialTask(self, t) for t in tasks]
623
624
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700625 def get_host_special_tasks(self, host_id, **data):
626 tasks = self.run('get_host_special_tasks',
627 host_id=host_id, **data)
628 return [SpecialTask(self, t) for t in tasks]
629
630
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700631 def get_host_status_task(self, host_id, end_time):
632 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700633 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700634 return SpecialTask(self, task) if task else None
635
636
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700637 def get_host_diagnosis_interval(self, host_id, end_time, success):
638 return self.run('get_host_diagnosis_interval',
639 host_id=host_id, end_time=end_time,
640 success=success)
641
642
Allen Li352b86a2016-12-14 12:11:27 -0800643 def create_job(self, control_file, name=' ',
644 priority=priorities.Priority.DEFAULT,
645 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT,
646 **dargs):
mbligh67647152008-11-19 00:18:14 +0000647 id = self.run('create_job', name=name, priority=priority,
648 control_file=control_file, control_type=control_type, **dargs)
649 return self.get_jobs(id=id)[0]
650
651
Simran Basi01984f52015-10-12 15:36:45 -0700652 def abort_jobs(self, jobs):
653 """Abort a list of jobs.
654
655 Already completed jobs will not be affected.
656
657 @param jobs: List of job ids to abort.
658 """
659 for job in jobs:
660 self.run('abort_host_queue_entries', job_id=job)
661
662
Kevin Cheng19521982016-09-22 12:27:23 -0700663 def get_hosts_by_attribute(self, attribute, value):
664 """
665 Get the list of hosts that share the same host attribute value.
666
667 @param attribute: String of the host attribute to check.
668 @param value: String of the value that is shared between hosts.
669
670 @returns List of hostnames that all have the same host attribute and
671 value.
672 """
673 return self.run('get_hosts_by_attribute',
674 attribute=attribute, value=value)
675
676
677 def lock_host(self, host, lock_reason, fail_if_locked=False):
678 """
679 Lock the given host with the given lock reason.
680
681 Locking a host that's already locked using the 'modify_hosts' rpc
682 will raise an exception. That's why fail_if_locked exists so the
683 caller can determine if the lock succeeded or failed. This will
684 save every caller from wrapping lock_host in a try-except.
685
686 @param host: hostname of host to lock.
687 @param lock_reason: Reason for locking host.
688 @param fail_if_locked: Return False if host is already locked.
689
690 @returns Boolean, True if lock was successful, False otherwise.
691 """
692 try:
693 self.run('modify_hosts',
694 host_filter_data={'hostname': host},
695 update_data={'locked': True,
696 'lock_reason': lock_reason})
697 except Exception:
698 return not fail_if_locked
699 return True
700
701
702 def unlock_hosts(self, locked_hosts):
703 """
704 Unlock the hosts.
705
706 Unlocking a host that's already unlocked will do nothing so we don't
707 need any special try-except clause here.
708
709 @param locked_hosts: List of hostnames of hosts to unlock.
710 """
711 self.run('modify_hosts',
712 host_filter_data={'hostname__in': locked_hosts},
713 update_data={'locked': False,
714 'lock_reason': ''})
715
716
mbligh5280e3b2008-12-22 14:39:28 +0000717class TestResults(object):
718 """
719 Container class used to hold the results of the tests for a job
720 """
721 def __init__(self):
722 self.good = []
723 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000724 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000725
726
727 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000728 if result.complete_count > result.pass_count:
729 self.fail.append(result)
730 elif result.incomplete_count > 0:
731 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000732 else:
mbligh451ede12009-02-12 21:54:03 +0000733 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000734
735
736class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000737 """
738 Generic object used to construct python objects from rpc calls
739 """
740 def __init__(self, afe, hash):
741 self.afe = afe
742 self.hash = hash
743 self.__dict__.update(hash)
744
745
746 def __str__(self):
747 return dump_object(self.__repr__(), self)
748
749
mbligh1354c9d2008-12-22 14:56:13 +0000750class ControlFile(RpcObject):
751 """
752 AFE control file object
753
754 Fields: synch_count, dependencies, control_file, is_server
755 """
756 def __repr__(self):
757 return 'CONTROL FILE: %s' % self.control_file
758
759
mbligh5280e3b2008-12-22 14:39:28 +0000760class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000761 """
762 AFE label object
763
764 Fields:
765 name, invalid, platform, kernel_config, id, only_if_needed
766 """
767 def __repr__(self):
768 return 'LABEL: %s' % self.name
769
770
771 def add_hosts(self, hosts):
Prathmesh Prabhuef7e5b02017-11-08 11:57:47 -0800772 # We must use the label's name instead of the id because label ids are
773 # not consistent across master-shard.
774 return self.afe.run('label_add_hosts', id=self.name, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000775
776
777 def remove_hosts(self, hosts):
Prathmesh Prabhuef7e5b02017-11-08 11:57:47 -0800778 # We must use the label's name instead of the id because label ids are
779 # not consistent across master-shard.
780 return self.afe.run('label_remove_hosts', id=self.name, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000781
782
mbligh5280e3b2008-12-22 14:39:28 +0000783class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000784 """
785 AFE acl object
786
787 Fields:
788 users, hosts, description, name, id
789 """
790 def __repr__(self):
791 return 'ACL: %s' % self.name
792
793
794 def add_hosts(self, hosts):
795 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
796 return self.afe.run('acl_group_add_hosts', self.id, hosts)
797
798
799 def remove_hosts(self, hosts):
800 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
801 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
802
803
mbligh54459c72009-01-21 19:26:44 +0000804 def add_users(self, users):
805 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
806 return self.afe.run('acl_group_add_users', id=self.name, users=users)
807
808
mbligh5280e3b2008-12-22 14:39:28 +0000809class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000810 """
811 AFE job object
812
813 Fields:
814 name, control_file, control_type, synch_count, reboot_before,
815 run_verify, priority, email_list, created_on, dependencies,
816 timeout, owner, reboot_after, id
817 """
818 def __repr__(self):
819 return 'JOB: %s' % self.id
820
821
mbligh5280e3b2008-12-22 14:39:28 +0000822class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000823 """
824 AFE job_status object
825
826 Fields:
827 status, complete, deleted, meta_host, host, active, execution_subdir, id
828 """
829 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800830 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000831 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700832 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000833 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000834
835
836 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000837 if self.host and self.host.hostname:
838 hostname = self.host.hostname
839 else:
840 hostname = 'None'
841 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000842
843
MK Ryu1b2d7f92015-02-24 17:45:02 -0800844class SpecialTask(RpcObject):
845 """
846 AFE special task object
847 """
848 def __init__(self, afe, hash):
849 super(SpecialTask, self).__init__(afe, hash)
850 self.host = Host(afe, self.host)
851
852
853 def __repr__(self):
854 return 'SPECIAL TASK: %s' % self.id
855
856
mbligh5280e3b2008-12-22 14:39:28 +0000857class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000858 """
859 AFE host object
860
861 Fields:
862 status, lock_time, locked_by, locked, hostname, invalid,
Allen Lie4c08272017-02-01 16:40:53 -0800863 labels, platform, protection, dirty, id
mbligh67647152008-11-19 00:18:14 +0000864 """
865 def __repr__(self):
866 return 'HOST OBJECT: %s' % self.hostname
867
868
869 def show(self):
870 labels = list(set(self.labels) - set([self.platform]))
871 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
872 self.locked, self.platform,
873 ', '.join(labels))
874
875
mbligh54459c72009-01-21 19:26:44 +0000876 def delete(self):
877 return self.afe.run('delete_host', id=self.id)
878
879
mbligh6463c4b2009-01-30 00:33:37 +0000880 def modify(self, **dargs):
881 return self.afe.run('modify_host', id=self.id, **dargs)
882
883
mbligh67647152008-11-19 00:18:14 +0000884 def get_acls(self):
885 return self.afe.get_acls(hosts__hostname=self.hostname)
886
887
888 def add_acl(self, acl_name):
889 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
890 return self.afe.run('acl_group_add_hosts', id=acl_name,
891 hosts=[self.hostname])
892
893
894 def remove_acl(self, acl_name):
895 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
896 return self.afe.run('acl_group_remove_hosts', id=acl_name,
897 hosts=[self.hostname])
898
899
900 def get_labels(self):
901 return self.afe.get_labels(host__hostname__in=[self.hostname])
902
903
904 def add_labels(self, labels):
905 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
906 return self.afe.run('host_add_labels', id=self.id, labels=labels)
907
908
909 def remove_labels(self, labels):
910 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
911 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000912
913
Allen Lif74957d2017-11-20 17:46:48 -0800914 def is_available(self):
915 """Check whether DUT host is available.
916
917 @return: bool
918 """
919 return not (self.locked
920 or self.status in host_states.UNAVAILABLE_STATES)
921
922
mbligh54459c72009-01-21 19:26:44 +0000923class User(RpcObject):
924 def __repr__(self):
925 return 'USER: %s' % self.login
926
927
mbligh5280e3b2008-12-22 14:39:28 +0000928class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000929 """
930 TKO test status object
931
932 Fields:
933 test_idx, hostname, testname, id
934 complete_count, incomplete_count, group_count, pass_count
935 """
936 def __repr__(self):
937 return 'TEST STATUS: %s' % self.id
938
939
MK Ryuacf35922014-10-03 14:56:49 -0700940class HostAttribute(RpcObject):
941 """
942 AFE host attribute object
943
944 Fields:
945 id, host, attribute, value
946 """
947 def __repr__(self):
948 return 'HOST ATTRIBUTE %d' % self.id