blob: c0679d5cafaee150a3dc8cb34dc2d6720d7d60bd [file] [log] [blame]
mbligh67647152008-11-19 00:18:14 +00001# Copyright Martin J. Bligh, Google Inc 2008
2# Released under the GPL v2
3
4"""
5This class allows you to communicate with the frontend to submit jobs etc
6It is designed for writing more sophisiticated server-side control files that
7can recursively add and manage other jobs.
8
9We turn the JSON dictionaries into real objects that are more idiomatic
10
mblighc31e4022008-12-11 19:32:30 +000011For docs, see:
Aviv Keshet2c709f62013-05-07 12:52:15 -070012 http://www.chromium.org/chromium-os/testing/afe-rpc-infrastructure
mblighc31e4022008-12-11 19:32:30 +000013 http://docs.djangoproject.com/en/dev/ref/models/querysets/#queryset-api
mbligh67647152008-11-19 00:18:14 +000014"""
15
Richard Barnetteb9b37982016-05-20 19:23:39 -070016#pylint: disable=missing-docstring
17
Dan Shie8e0c052015-09-01 00:27:27 -070018import getpass
19import os
20import re
Dan Shie8e0c052015-09-01 00:27:27 -070021
mbligh67647152008-11-19 00:18:14 +000022import common
Allen Li327e6fd2016-11-22 13:45:41 -080023
mbligh67647152008-11-19 00:18:14 +000024from autotest_lib.frontend.afe import rpc_client_lib
Dan Shie8e0c052015-09-01 00:27:27 -070025from autotest_lib.client.common_lib import control_data
mbligh37eceaa2008-12-15 22:56:37 +000026from autotest_lib.client.common_lib import global_config
Allen Li352b86a2016-12-14 12:11:27 -080027from autotest_lib.client.common_lib import priorities
mbligh67647152008-11-19 00:18:14 +000028from autotest_lib.client.common_lib import utils
Scott Zawalski63470dd2012-09-05 00:49:43 -040029from autotest_lib.tko import db
30
Dan Shi5e2efb72017-02-07 11:40:23 -080031try:
32 from chromite.lib import metrics
33except ImportError:
34 metrics = utils.metrics_mock
Scott Zawalski63470dd2012-09-05 00:49:43 -040035
mbligh4e576612008-12-22 14:56:36 +000036try:
37 from autotest_lib.server.site_common import site_utils as server_utils
38except:
39 from autotest_lib.server import utils as server_utils
40form_ntuples_from_machines = server_utils.form_ntuples_from_machines
mbligh67647152008-11-19 00:18:14 +000041
mbligh37eceaa2008-12-15 22:56:37 +000042GLOBAL_CONFIG = global_config.global_config
43DEFAULT_SERVER = 'autotest'
44
Dan Shie8e0c052015-09-01 00:27:27 -070045
mbligh67647152008-11-19 00:18:14 +000046def dump_object(header, obj):
47 """
48 Standard way to print out the frontend objects (eg job, host, acl, label)
49 in a human-readable fashion for debugging
50 """
51 result = header + '\n'
52 for key in obj.hash:
53 if key == 'afe' or key == 'hash':
54 continue
55 result += '%20s: %s\n' % (key, obj.hash[key])
56 return result
57
58
mbligh5280e3b2008-12-22 14:39:28 +000059class RpcClient(object):
mbligh67647152008-11-19 00:18:14 +000060 """
mbligh451ede12009-02-12 21:54:03 +000061 Abstract RPC class for communicating with the autotest frontend
62 Inherited for both TKO and AFE uses.
mbligh67647152008-11-19 00:18:14 +000063
mbligh1ef218d2009-08-03 16:57:56 +000064 All the constructors go in the afe / tko class.
mbligh451ede12009-02-12 21:54:03 +000065 Manipulating methods go in the object classes themselves
mbligh67647152008-11-19 00:18:14 +000066 """
mbligh99b24f42009-06-08 16:45:55 +000067 def __init__(self, path, user, server, print_log, debug, reply_debug):
mbligh67647152008-11-19 00:18:14 +000068 """
mbligh451ede12009-02-12 21:54:03 +000069 Create a cached instance of a connection to the frontend
mbligh67647152008-11-19 00:18:14 +000070
71 user: username to connect as
mbligh451ede12009-02-12 21:54:03 +000072 server: frontend server to connect to
mbligh67647152008-11-19 00:18:14 +000073 print_log: pring a logging message to stdout on every operation
74 debug: print out all RPC traffic
75 """
Dan Shiff78f112015-06-12 13:34:02 -070076 if not user and utils.is_in_container():
77 user = GLOBAL_CONFIG.get_config_value('SSP', 'user', default=None)
mblighc31e4022008-12-11 19:32:30 +000078 if not user:
mblighdb59e3c2009-11-21 01:45:18 +000079 user = getpass.getuser()
mbligh451ede12009-02-12 21:54:03 +000080 if not server:
mbligh475f7762009-01-30 00:34:04 +000081 if 'AUTOTEST_WEB' in os.environ:
mbligh451ede12009-02-12 21:54:03 +000082 server = os.environ['AUTOTEST_WEB']
mbligh475f7762009-01-30 00:34:04 +000083 else:
mbligh451ede12009-02-12 21:54:03 +000084 server = GLOBAL_CONFIG.get_config_value('SERVER', 'hostname',
85 default=DEFAULT_SERVER)
86 self.server = server
mbligh67647152008-11-19 00:18:14 +000087 self.user = user
88 self.print_log = print_log
89 self.debug = debug
mbligh99b24f42009-06-08 16:45:55 +000090 self.reply_debug = reply_debug
Scott Zawalski347aaf42012-04-03 16:33:00 -040091 headers = {'AUTHORIZATION': self.user}
92 rpc_server = 'http://' + server + path
mbligh1354c9d2008-12-22 14:56:13 +000093 if debug:
94 print 'SERVER: %s' % rpc_server
95 print 'HEADERS: %s' % headers
mbligh67647152008-11-19 00:18:14 +000096 self.proxy = rpc_client_lib.get_proxy(rpc_server, headers=headers)
97
98
99 def run(self, call, **dargs):
100 """
101 Make a RPC call to the AFE server
102 """
103 rpc_call = getattr(self.proxy, call)
104 if self.debug:
105 print 'DEBUG: %s %s' % (call, dargs)
mbligh451ede12009-02-12 21:54:03 +0000106 try:
mbligh99b24f42009-06-08 16:45:55 +0000107 result = utils.strip_unicode(rpc_call(**dargs))
108 if self.reply_debug:
109 print result
110 return result
mbligh451ede12009-02-12 21:54:03 +0000111 except Exception:
mbligh451ede12009-02-12 21:54:03 +0000112 raise
mbligh67647152008-11-19 00:18:14 +0000113
114
115 def log(self, message):
116 if self.print_log:
117 print message
118
119
jamesrenc3940222010-02-19 21:57:37 +0000120class Planner(RpcClient):
121 def __init__(self, user=None, server=None, print_log=True, debug=False,
122 reply_debug=False):
123 super(Planner, self).__init__(path='/planner/server/rpc/',
124 user=user,
125 server=server,
126 print_log=print_log,
127 debug=debug,
128 reply_debug=reply_debug)
129
130
mbligh5280e3b2008-12-22 14:39:28 +0000131class TKO(RpcClient):
mbligh99b24f42009-06-08 16:45:55 +0000132 def __init__(self, user=None, server=None, print_log=True, debug=False,
133 reply_debug=False):
Scott Zawalski347aaf42012-04-03 16:33:00 -0400134 super(TKO, self).__init__(path='/new_tko/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000135 user=user,
136 server=server,
137 print_log=print_log,
138 debug=debug,
139 reply_debug=reply_debug)
Scott Zawalski63470dd2012-09-05 00:49:43 -0400140 self._db = None
141
142
Allen Li327e6fd2016-11-22 13:45:41 -0800143 @metrics.SecondsTimerDecorator(
Prathmesh Prabhua7556a92017-02-01 14:18:41 -0800144 'chromeos/autotest/tko/get_job_status_duration')
Scott Zawalski63470dd2012-09-05 00:49:43 -0400145 def get_job_test_statuses_from_db(self, job_id):
146 """Get job test statuses from the database.
147
148 Retrieve a set of fields from a job that reflect the status of each test
149 run within a job.
150 fields retrieved: status, test_name, reason, test_started_time,
151 test_finished_time, afe_job_id, job_owner, hostname.
152
153 @param job_id: The afe job id to look up.
154 @returns a TestStatus object of the resulting information.
155 """
156 if self._db is None:
Dan Shie8e0c052015-09-01 00:27:27 -0700157 self._db = db.db()
Fang Deng5c508332014-03-19 10:26:00 -0700158 fields = ['status', 'test_name', 'subdir', 'reason',
159 'test_started_time', 'test_finished_time', 'afe_job_id',
160 'job_owner', 'hostname', 'job_tag']
Scott Zawalski63470dd2012-09-05 00:49:43 -0400161 table = 'tko_test_view_2'
162 where = 'job_tag like "%s-%%"' % job_id
163 test_status = []
164 # Run commit before we query to ensure that we are pulling the latest
165 # results.
166 self._db.commit()
167 for entry in self._db.select(','.join(fields), table, (where, None)):
168 status_dict = {}
169 for key,value in zip(fields, entry):
170 # All callers expect values to be a str object.
171 status_dict[key] = str(value)
172 # id is used by TestStatus to uniquely identify each Test Status
173 # obj.
174 status_dict['id'] = [status_dict['reason'], status_dict['hostname'],
175 status_dict['test_name']]
176 test_status.append(status_dict)
177
178 return [TestStatus(self, e) for e in test_status]
mblighc31e4022008-12-11 19:32:30 +0000179
180
181 def get_status_counts(self, job, **data):
182 entries = self.run('get_status_counts',
mbligh1ef218d2009-08-03 16:57:56 +0000183 group_by=['hostname', 'test_name', 'reason'],
mblighc31e4022008-12-11 19:32:30 +0000184 job_tag__startswith='%s-' % job, **data)
mbligh5280e3b2008-12-22 14:39:28 +0000185 return [TestStatus(self, e) for e in entries['groups']]
mblighc31e4022008-12-11 19:32:30 +0000186
187
Richard Barnette260cbd02016-10-06 12:23:28 -0700188class _StableVersionMap(object):
189 """
190 A mapping from board names to strings naming software versions.
191
192 The mapping is meant to allow finding a nominally "stable" version
193 of software associated with a given board. The mapping identifies
194 specific versions of software that should be installed during
195 operations such as repair.
196
197 Conceptually, there are multiple version maps, each handling
198 different types of image. For instance, a single board may have
199 both a stable OS image (e.g. for CrOS), and a separate stable
200 firmware image.
201
202 Each different type of image requires a certain amount of special
203 handling, implemented by a subclass of `StableVersionMap`. The
204 subclasses take care of pre-processing of arguments, delegating
205 actual RPC calls to this superclass.
206
207 @property _afe AFE object through which to make the actual RPC
208 calls.
209 @property _android Value of the `android` parameter to be passed
210 when calling the `get_stable_version` RPC.
211 """
212
213 # DEFAULT_BOARD - The stable_version RPC API recognizes this special
214 # name as a mapping to use when no specific mapping for a board is
215 # present. This default mapping is only allowed for CrOS image
216 # types; other image type subclasses exclude it.
217 #
218 # TODO(jrbarnette): This value is copied from
219 # site_utils.stable_version_utils, because if we import that
220 # module here, it breaks unit tests. Something about the Django
221 # setup...
222 DEFAULT_BOARD = 'DEFAULT'
223
224
225 def __init__(self, afe, android):
226 self._afe = afe
227 self._android = android
228
229
230 def get_all_versions(self):
231 """
232 Get all mappings in the stable versions table.
233
234 Extracts the full content of the `stable_version` table
235 in the AFE database, and returns it as a dictionary
236 mapping board names to version strings.
237
238 @return A dictionary mapping board names to version strings.
239 """
240 return self._afe.run('get_all_stable_versions')
241
242
243 def get_version(self, board):
244 """
245 Get the mapping of one board in the stable versions table.
246
247 Look up and return the version mapped to the given board in the
248 `stable_versions` table in the AFE database.
249
250 @param board The board to be looked up.
251
252 @return The version mapped for the given board.
253 """
254 return self._afe.run('get_stable_version',
255 board=board, android=self._android)
256
257
258 def set_version(self, board, version):
259 """
260 Change the mapping of one board in the stable versions table.
261
262 Set the mapping in the `stable_versions` table in the AFE
263 database for the given board to the given version.
264
265 @param board The board to be updated.
266 @param version The new version to be assigned to the board.
267 """
268 self._afe.run('set_stable_version',
269 version=version, board=board)
270
271
272 def delete_version(self, board):
273 """
274 Remove the mapping of one board in the stable versions table.
275
276 Remove the mapping in the `stable_versions` table in the AFE
277 database for the given board.
278
279 @param board The board to be updated.
280 """
281 self._afe.run('delete_stable_version', board=board)
282
283
284class _OSVersionMap(_StableVersionMap):
285 """
286 Abstract stable version mapping for full OS images of various types.
287 """
288
289 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700290 # TODO(jrbarnette): We exclude non-OS (i.e. firmware) version
291 # mappings, but the returned dict doesn't distinguish CrOS
292 # boards from Android boards; both will be present, and the
293 # subclass can't distinguish them.
Richard Barnette260cbd02016-10-06 12:23:28 -0700294 #
295 # Ultimately, the right fix is to move knowledge of image type
296 # over to the RPC server side.
297 #
298 versions = super(_OSVersionMap, self).get_all_versions()
299 for board in versions.keys():
300 if '/' in board:
301 del versions[board]
302 return versions
303
304
305class _CrosVersionMap(_OSVersionMap):
306 """
307 Stable version mapping for Chrome OS release images.
308
309 This class manages a mapping of Chrome OS board names to known-good
310 release (or canary) images. The images selected can be installed on
311 DUTs during repair tasks, as a way of getting a DUT into a known
312 working state.
313 """
314
315 def __init__(self, afe):
316 super(_CrosVersionMap, self).__init__(afe, False)
317
Richard Barnette728e36f2016-11-03 16:04:29 -0700318 @staticmethod
319 def format_image_name(board, version):
320 """
321 Return an image name for a given `board` and `version`.
322
323 This formats `board` and `version` into a string identifying an
324 image file. The string represents part of a URL for access to
325 the image.
326
327 The returned image name is typically of a form like
328 "falco-release/R55-8872.44.0".
329 """
330 build_pattern = GLOBAL_CONFIG.get_config_value(
331 'CROS', 'stable_build_pattern')
332 return build_pattern % (board, version)
Richard Barnette260cbd02016-10-06 12:23:28 -0700333
Richard Barnette383ef9c2016-12-13 11:56:49 -0800334 def get_image_name(self, board):
335 """
336 Return the full image name of the stable version for `board`.
337
338 This finds the stable version for `board`, and returns a string
Richard Barnette728e36f2016-11-03 16:04:29 -0700339 identifying the associated image as for `format_image_name()`,
340 above.
Richard Barnette383ef9c2016-12-13 11:56:49 -0800341
342 @return A string identifying the image file for the stable
343 image for `board`.
344 """
Richard Barnette728e36f2016-11-03 16:04:29 -0700345 return self.format_image_name(board, self.get_version(board))
Richard Barnette260cbd02016-10-06 12:23:28 -0700346
347
348class _AndroidVersionMap(_OSVersionMap):
349 """
350 Stable version mapping for Android release images.
351
352 This class manages a mapping of Android/Brillo board names to
353 known-good images.
354 """
355
356 def __init__(self, afe):
357 super(_AndroidVersionMap, self).__init__(afe, True)
358
359
360 def get_all_versions(self):
361 versions = super(_AndroidVersionMap, self).get_all_versions()
362 del versions[self.DEFAULT_BOARD]
363 return versions
364
365
Richard Barnettee50453e2016-10-10 16:43:44 -0700366class _SuffixHackVersionMap(_StableVersionMap):
Richard Barnette260cbd02016-10-06 12:23:28 -0700367 """
Richard Barnettee50453e2016-10-10 16:43:44 -0700368 Abstract super class for mappings using a pseudo-board name.
Richard Barnette260cbd02016-10-06 12:23:28 -0700369
Richard Barnettee50453e2016-10-10 16:43:44 -0700370 For non-OS image type mappings, we look them up in the
371 `stable_versions` table by constructing a "pseudo-board" from the
372 real board name plus a suffix string that identifies the image type.
373 So, for instance the name "lulu/firmware" is used to look up the
374 FAFT firmware version for lulu boards.
Richard Barnette260cbd02016-10-06 12:23:28 -0700375 """
376
Richard Barnettee50453e2016-10-10 16:43:44 -0700377 # _SUFFIX - The suffix used in constructing the "pseudo-board"
378 # lookup key. Each subclass must define this value for itself.
Richard Barnette260cbd02016-10-06 12:23:28 -0700379 #
Richard Barnettee50453e2016-10-10 16:43:44 -0700380 _SUFFIX = None
Richard Barnette260cbd02016-10-06 12:23:28 -0700381
382 def __init__(self, afe):
Richard Barnettee50453e2016-10-10 16:43:44 -0700383 super(_SuffixHackVersionMap, self).__init__(afe, False)
Richard Barnette260cbd02016-10-06 12:23:28 -0700384
385
386 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700387 # Get all the mappings from the AFE, extract just the mappings
388 # with our suffix, and replace the pseudo-board name keys with
Richard Barnette260cbd02016-10-06 12:23:28 -0700389 # the real board names.
390 #
391 all_versions = super(
Richard Barnettee50453e2016-10-10 16:43:44 -0700392 _SuffixHackVersionMap, self).get_all_versions()
Richard Barnette260cbd02016-10-06 12:23:28 -0700393 return {
394 board[0 : -len(self._SUFFIX)]: all_versions[board]
395 for board in all_versions.keys()
396 if board.endswith(self._SUFFIX)
397 }
398
399
400 def get_version(self, board):
Richard Barnettee50453e2016-10-10 16:43:44 -0700401 board += self._SUFFIX
402 return super(_SuffixHackVersionMap, self).get_version(board)
403
404
405 def set_version(self, board, version):
406 board += self._SUFFIX
407 super(_SuffixHackVersionMap, self).set_version(board, version)
408
409
410 def delete_version(self, board):
411 board += self._SUFFIX
412 super(_SuffixHackVersionMap, self).delete_version(board)
413
414
415class _FAFTVersionMap(_SuffixHackVersionMap):
416 """
417 Stable version mapping for firmware versions used in FAFT repair.
418
419 When DUTs used for FAFT fail repair, stable firmware may need to be
420 flashed directly from original tarballs. The FAFT firmware version
421 mapping finds the appropriate tarball for a given board.
422 """
423
424 _SUFFIX = '/firmware'
425
426 def get_version(self, board):
427 # If there's no mapping for `board`, the lookup will return the
428 # default CrOS version mapping. To eliminate that case, we
429 # require a '/' character in the version, since CrOS versions
430 # won't match that.
Richard Barnette260cbd02016-10-06 12:23:28 -0700431 #
432 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
433 # the right fix is to move handling to the RPC server side.
434 #
Richard Barnette260cbd02016-10-06 12:23:28 -0700435 version = super(_FAFTVersionMap, self).get_version(board)
436 return version if '/' in version else None
437
438
Richard Barnettee50453e2016-10-10 16:43:44 -0700439class _FirmwareVersionMap(_SuffixHackVersionMap):
440 """
441 Stable version mapping for firmware supplied in Chrome OS images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700442
Richard Barnettee50453e2016-10-10 16:43:44 -0700443 A Chrome OS image bundles a version of the firmware that the
444 device should update to when the OS version is installed during
445 AU.
Richard Barnette260cbd02016-10-06 12:23:28 -0700446
Richard Barnettee50453e2016-10-10 16:43:44 -0700447 Test images suppress the firmware update during AU. Instead, during
448 repair and verify we check installed firmware on a DUT, compare it
449 against the stable version mapping for the board, and update when
450 the DUT is out-of-date.
451 """
452
453 _SUFFIX = '/rwfw'
454
455 def get_version(self, board):
456 # If there's no mapping for `board`, the lookup will return the
457 # default CrOS version mapping. To eliminate that case, we
458 # require the version start with "Google_", since CrOS versions
459 # won't match that.
460 #
461 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
462 # the right fix is to move handling to the RPC server side.
463 #
464 version = super(_FirmwareVersionMap, self).get_version(board)
465 return version if version.startswith('Google_') else None
Richard Barnette260cbd02016-10-06 12:23:28 -0700466
467
mbligh5280e3b2008-12-22 14:39:28 +0000468class AFE(RpcClient):
mbligh1ef218d2009-08-03 16:57:56 +0000469
Richard Barnette260cbd02016-10-06 12:23:28 -0700470 # Known image types for stable version mapping objects.
471 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
472 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
Richard Barnettee50453e2016-10-10 16:43:44 -0700473 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700474 # ANDROID_IMAGE_TYPE - Mappings for Android images.
475 #
476 CROS_IMAGE_TYPE = 'cros'
477 FAFT_IMAGE_TYPE = 'faft'
Richard Barnettee50453e2016-10-10 16:43:44 -0700478 FIRMWARE_IMAGE_TYPE = 'firmware'
Richard Barnette260cbd02016-10-06 12:23:28 -0700479 ANDROID_IMAGE_TYPE = 'android'
480
481 _IMAGE_MAPPING_CLASSES = {
482 CROS_IMAGE_TYPE: _CrosVersionMap,
483 FAFT_IMAGE_TYPE: _FAFTVersionMap,
Richard Barnettee50453e2016-10-10 16:43:44 -0700484 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
Richard Barnette260cbd02016-10-06 12:23:28 -0700485 ANDROID_IMAGE_TYPE: _AndroidVersionMap
486 }
487
488
Prathmesh Prabhu6d5ba592017-01-05 13:56:04 -0800489 def __init__(self, user=None, server=None, print_log=True, debug=False,
490 reply_debug=False, job=None):
491 self.job = job
492 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
493 user=user,
494 server=server,
495 print_log=print_log,
496 debug=debug,
497 reply_debug=reply_debug)
498
499
Richard Barnette260cbd02016-10-06 12:23:28 -0700500 def get_stable_version_map(self, image_type):
501 """
502 Return a stable version mapping for the given image type.
503
504 @return An object mapping board names to version strings for
505 software of the given image type.
506 """
507 return self._IMAGE_MAPPING_CLASSES[image_type](self)
508
509
mbligh67647152008-11-19 00:18:14 +0000510 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000511 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000512 statuses = self.run('get_static_data')['host_statuses']
513 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000514 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000515 if live == False:
516 return dead_statuses
517 else:
518 return statuses
519
520
mbligh71094012009-12-19 05:35:21 +0000521 @staticmethod
522 def _dict_for_host_query(hostnames=(), status=None, label=None):
523 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000524 if hostnames:
525 query_args['hostname__in'] = hostnames
526 if status:
527 query_args['status'] = status
528 if label:
529 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000530 return query_args
531
532
533 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
534 query_args = dict(dargs)
535 query_args.update(self._dict_for_host_query(hostnames=hostnames,
536 status=status,
537 label=label))
538 hosts = self.run('get_hosts', **query_args)
539 return [Host(self, h) for h in hosts]
540
541
542 def get_hostnames(self, status=None, label=None, **dargs):
543 """Like get_hosts() but returns hostnames instead of Host objects."""
544 # This implementation can be replaced with a more efficient one
545 # that does not query for entire host objects in the future.
546 return [host_obj.hostname for host_obj in
547 self.get_hosts(status=status, label=label, **dargs)]
548
549
550 def reverify_hosts(self, hostnames=(), status=None, label=None):
551 query_args = dict(locked=False,
552 aclgroup__users__login=self.user)
553 query_args.update(self._dict_for_host_query(hostnames=hostnames,
554 status=status,
555 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000556 return self.run('reverify_hosts', **query_args)
557
558
mbligh67647152008-11-19 00:18:14 +0000559 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000560 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000561 return self.get_hosts(id=id)[0]
562
563
MK Ryuacf35922014-10-03 14:56:49 -0700564 def get_host_attribute(self, attr, **dargs):
565 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
566 return [HostAttribute(self, a) for a in host_attrs]
567
568
Chris Masone8abb6fc2012-01-31 09:27:36 -0800569 def set_host_attribute(self, attr, val, **dargs):
570 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
571
572
mbligh67647152008-11-19 00:18:14 +0000573 def get_labels(self, **dargs):
574 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000575 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000576
577
578 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000579 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000580 return self.get_labels(id=id)[0]
581
582
583 def get_acls(self, **dargs):
584 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000585 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000586
587
588 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000589 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000590 return self.get_acls(id=id)[0]
591
592
mbligh54459c72009-01-21 19:26:44 +0000593 def get_users(self, **dargs):
594 users = self.run('get_users', **dargs)
595 return [User(self, u) for u in users]
596
597
mbligh1354c9d2008-12-22 14:56:13 +0000598 def generate_control_file(self, tests, **dargs):
599 ret = self.run('generate_control_file', tests=tests, **dargs)
600 return ControlFile(self, ret)
601
602
mbligh67647152008-11-19 00:18:14 +0000603 def get_jobs(self, summary=False, **dargs):
604 if summary:
605 jobs_data = self.run('get_jobs_summary', **dargs)
606 else:
607 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000608 jobs = []
609 for j in jobs_data:
610 job = Job(self, j)
611 # Set up some extra information defaults
612 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
613 job.platform_results = {}
614 job.platform_reasons = {}
615 jobs.append(job)
616 return jobs
mbligh67647152008-11-19 00:18:14 +0000617
618
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700619 def get_host_queue_entries(self, **kwargs):
620 """Find JobStatus objects matching some constraints.
mbligh99b24f42009-06-08 16:45:55 +0000621
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700622 @param **kwargs: Arguments to pass to the RPC
623 """
624 entries = self.run('get_host_queue_entries', **kwargs)
625 return self._entries_to_statuses(entries)
626
627
628 def get_host_queue_entries_by_insert_time(self, **kwargs):
629 """Like get_host_queue_entries, but using the insert index table.
630
631 @param **kwargs: Arguments to pass to the RPC
632 """
633 entries = self.run('get_host_queue_entries_by_insert_time', **kwargs)
634 return self._entries_to_statuses(entries)
635
636
637 def _entries_to_statuses(self, entries):
638 """Converts HQEs to JobStatuses
639
640 Sadly, get_host_queue_entries doesn't return platforms, we have
641 to get those back from an explicit get_hosts queury, then patch
642 the new host objects back into the host list.
643
644 :param entries: A list of HQEs from get_host_queue_entries or
645 get_host_queue_entries_by_insert_time.
646 """
647 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000648 hostnames = [s.host.hostname for s in job_statuses if s.host]
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700649 hosts = {}
mbligh99b24f42009-06-08 16:45:55 +0000650 for host in self.get_hosts(hostname__in=hostnames):
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700651 hosts[host.hostname] = host
mbligh99b24f42009-06-08 16:45:55 +0000652 for status in job_statuses:
653 if status.host:
Paul Hobbsf6fbe2d2017-06-07 14:17:07 -0700654 status.host = hosts.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000655 # filter job statuses that have either host or meta_host
656 return [status for status in job_statuses if (status.host or
657 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000658
659
MK Ryu1b2d7f92015-02-24 17:45:02 -0800660 def get_special_tasks(self, **data):
661 tasks = self.run('get_special_tasks', **data)
662 return [SpecialTask(self, t) for t in tasks]
663
664
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700665 def get_host_special_tasks(self, host_id, **data):
666 tasks = self.run('get_host_special_tasks',
667 host_id=host_id, **data)
668 return [SpecialTask(self, t) for t in tasks]
669
670
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700671 def get_host_status_task(self, host_id, end_time):
672 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700673 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700674 return SpecialTask(self, task) if task else None
675
676
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700677 def get_host_diagnosis_interval(self, host_id, end_time, success):
678 return self.run('get_host_diagnosis_interval',
679 host_id=host_id, end_time=end_time,
680 success=success)
681
682
Allen Li352b86a2016-12-14 12:11:27 -0800683 def create_job(self, control_file, name=' ',
684 priority=priorities.Priority.DEFAULT,
685 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT,
686 **dargs):
mbligh67647152008-11-19 00:18:14 +0000687 id = self.run('create_job', name=name, priority=priority,
688 control_file=control_file, control_type=control_type, **dargs)
689 return self.get_jobs(id=id)[0]
690
691
Simran Basi01984f52015-10-12 15:36:45 -0700692 def abort_jobs(self, jobs):
693 """Abort a list of jobs.
694
695 Already completed jobs will not be affected.
696
697 @param jobs: List of job ids to abort.
698 """
699 for job in jobs:
700 self.run('abort_host_queue_entries', job_id=job)
701
702
Kevin Cheng19521982016-09-22 12:27:23 -0700703 def get_hosts_by_attribute(self, attribute, value):
704 """
705 Get the list of hosts that share the same host attribute value.
706
707 @param attribute: String of the host attribute to check.
708 @param value: String of the value that is shared between hosts.
709
710 @returns List of hostnames that all have the same host attribute and
711 value.
712 """
713 return self.run('get_hosts_by_attribute',
714 attribute=attribute, value=value)
715
716
717 def lock_host(self, host, lock_reason, fail_if_locked=False):
718 """
719 Lock the given host with the given lock reason.
720
721 Locking a host that's already locked using the 'modify_hosts' rpc
722 will raise an exception. That's why fail_if_locked exists so the
723 caller can determine if the lock succeeded or failed. This will
724 save every caller from wrapping lock_host in a try-except.
725
726 @param host: hostname of host to lock.
727 @param lock_reason: Reason for locking host.
728 @param fail_if_locked: Return False if host is already locked.
729
730 @returns Boolean, True if lock was successful, False otherwise.
731 """
732 try:
733 self.run('modify_hosts',
734 host_filter_data={'hostname': host},
735 update_data={'locked': True,
736 'lock_reason': lock_reason})
737 except Exception:
738 return not fail_if_locked
739 return True
740
741
742 def unlock_hosts(self, locked_hosts):
743 """
744 Unlock the hosts.
745
746 Unlocking a host that's already unlocked will do nothing so we don't
747 need any special try-except clause here.
748
749 @param locked_hosts: List of hostnames of hosts to unlock.
750 """
751 self.run('modify_hosts',
752 host_filter_data={'hostname__in': locked_hosts},
753 update_data={'locked': False,
754 'lock_reason': ''})
755
756
mbligh5280e3b2008-12-22 14:39:28 +0000757class TestResults(object):
758 """
759 Container class used to hold the results of the tests for a job
760 """
761 def __init__(self):
762 self.good = []
763 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000764 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000765
766
767 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000768 if result.complete_count > result.pass_count:
769 self.fail.append(result)
770 elif result.incomplete_count > 0:
771 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000772 else:
mbligh451ede12009-02-12 21:54:03 +0000773 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000774
775
776class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000777 """
778 Generic object used to construct python objects from rpc calls
779 """
780 def __init__(self, afe, hash):
781 self.afe = afe
782 self.hash = hash
783 self.__dict__.update(hash)
784
785
786 def __str__(self):
787 return dump_object(self.__repr__(), self)
788
789
mbligh1354c9d2008-12-22 14:56:13 +0000790class ControlFile(RpcObject):
791 """
792 AFE control file object
793
794 Fields: synch_count, dependencies, control_file, is_server
795 """
796 def __repr__(self):
797 return 'CONTROL FILE: %s' % self.control_file
798
799
mbligh5280e3b2008-12-22 14:39:28 +0000800class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000801 """
802 AFE label object
803
804 Fields:
805 name, invalid, platform, kernel_config, id, only_if_needed
806 """
807 def __repr__(self):
808 return 'LABEL: %s' % self.name
809
810
811 def add_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800812 return self.afe.run('label_add_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000813
814
815 def remove_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800816 return self.afe.run('label_remove_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000817
818
mbligh5280e3b2008-12-22 14:39:28 +0000819class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000820 """
821 AFE acl object
822
823 Fields:
824 users, hosts, description, name, id
825 """
826 def __repr__(self):
827 return 'ACL: %s' % self.name
828
829
830 def add_hosts(self, hosts):
831 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
832 return self.afe.run('acl_group_add_hosts', self.id, hosts)
833
834
835 def remove_hosts(self, hosts):
836 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
837 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
838
839
mbligh54459c72009-01-21 19:26:44 +0000840 def add_users(self, users):
841 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
842 return self.afe.run('acl_group_add_users', id=self.name, users=users)
843
844
mbligh5280e3b2008-12-22 14:39:28 +0000845class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000846 """
847 AFE job object
848
849 Fields:
850 name, control_file, control_type, synch_count, reboot_before,
851 run_verify, priority, email_list, created_on, dependencies,
852 timeout, owner, reboot_after, id
853 """
854 def __repr__(self):
855 return 'JOB: %s' % self.id
856
857
mbligh5280e3b2008-12-22 14:39:28 +0000858class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000859 """
860 AFE job_status object
861
862 Fields:
863 status, complete, deleted, meta_host, host, active, execution_subdir, id
864 """
865 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800866 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000867 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700868 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000869 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000870
871
872 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000873 if self.host and self.host.hostname:
874 hostname = self.host.hostname
875 else:
876 hostname = 'None'
877 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000878
879
MK Ryu1b2d7f92015-02-24 17:45:02 -0800880class SpecialTask(RpcObject):
881 """
882 AFE special task object
883 """
884 def __init__(self, afe, hash):
885 super(SpecialTask, self).__init__(afe, hash)
886 self.host = Host(afe, self.host)
887
888
889 def __repr__(self):
890 return 'SPECIAL TASK: %s' % self.id
891
892
mbligh5280e3b2008-12-22 14:39:28 +0000893class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000894 """
895 AFE host object
896
897 Fields:
898 status, lock_time, locked_by, locked, hostname, invalid,
Allen Lie4c08272017-02-01 16:40:53 -0800899 labels, platform, protection, dirty, id
mbligh67647152008-11-19 00:18:14 +0000900 """
901 def __repr__(self):
902 return 'HOST OBJECT: %s' % self.hostname
903
904
905 def show(self):
906 labels = list(set(self.labels) - set([self.platform]))
907 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
908 self.locked, self.platform,
909 ', '.join(labels))
910
911
mbligh54459c72009-01-21 19:26:44 +0000912 def delete(self):
913 return self.afe.run('delete_host', id=self.id)
914
915
mbligh6463c4b2009-01-30 00:33:37 +0000916 def modify(self, **dargs):
917 return self.afe.run('modify_host', id=self.id, **dargs)
918
919
mbligh67647152008-11-19 00:18:14 +0000920 def get_acls(self):
921 return self.afe.get_acls(hosts__hostname=self.hostname)
922
923
924 def add_acl(self, acl_name):
925 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
926 return self.afe.run('acl_group_add_hosts', id=acl_name,
927 hosts=[self.hostname])
928
929
930 def remove_acl(self, acl_name):
931 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
932 return self.afe.run('acl_group_remove_hosts', id=acl_name,
933 hosts=[self.hostname])
934
935
936 def get_labels(self):
937 return self.afe.get_labels(host__hostname__in=[self.hostname])
938
939
940 def add_labels(self, labels):
941 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
942 return self.afe.run('host_add_labels', id=self.id, labels=labels)
943
944
945 def remove_labels(self, labels):
946 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
947 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000948
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