blob: 35ec6489a91fed8a32c5141e8b86165ad6889790 [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 -080023from chromite.lib import metrics
24
mbligh67647152008-11-19 00:18:14 +000025from autotest_lib.frontend.afe import rpc_client_lib
Dan Shie8e0c052015-09-01 00:27:27 -070026from autotest_lib.client.common_lib import control_data
mbligh37eceaa2008-12-15 22:56:37 +000027from autotest_lib.client.common_lib import global_config
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
31
mbligh4e576612008-12-22 14:56:36 +000032try:
33 from autotest_lib.server.site_common import site_utils as server_utils
34except:
35 from autotest_lib.server import utils as server_utils
36form_ntuples_from_machines = server_utils.form_ntuples_from_machines
mbligh67647152008-11-19 00:18:14 +000037
mbligh37eceaa2008-12-15 22:56:37 +000038GLOBAL_CONFIG = global_config.global_config
39DEFAULT_SERVER = 'autotest'
40
Dan Shie8e0c052015-09-01 00:27:27 -070041
mbligh67647152008-11-19 00:18:14 +000042def dump_object(header, obj):
43 """
44 Standard way to print out the frontend objects (eg job, host, acl, label)
45 in a human-readable fashion for debugging
46 """
47 result = header + '\n'
48 for key in obj.hash:
49 if key == 'afe' or key == 'hash':
50 continue
51 result += '%20s: %s\n' % (key, obj.hash[key])
52 return result
53
54
mbligh5280e3b2008-12-22 14:39:28 +000055class RpcClient(object):
mbligh67647152008-11-19 00:18:14 +000056 """
mbligh451ede12009-02-12 21:54:03 +000057 Abstract RPC class for communicating with the autotest frontend
58 Inherited for both TKO and AFE uses.
mbligh67647152008-11-19 00:18:14 +000059
mbligh1ef218d2009-08-03 16:57:56 +000060 All the constructors go in the afe / tko class.
mbligh451ede12009-02-12 21:54:03 +000061 Manipulating methods go in the object classes themselves
mbligh67647152008-11-19 00:18:14 +000062 """
mbligh99b24f42009-06-08 16:45:55 +000063 def __init__(self, path, user, server, print_log, debug, reply_debug):
mbligh67647152008-11-19 00:18:14 +000064 """
mbligh451ede12009-02-12 21:54:03 +000065 Create a cached instance of a connection to the frontend
mbligh67647152008-11-19 00:18:14 +000066
67 user: username to connect as
mbligh451ede12009-02-12 21:54:03 +000068 server: frontend server to connect to
mbligh67647152008-11-19 00:18:14 +000069 print_log: pring a logging message to stdout on every operation
70 debug: print out all RPC traffic
71 """
Dan Shiff78f112015-06-12 13:34:02 -070072 if not user and utils.is_in_container():
73 user = GLOBAL_CONFIG.get_config_value('SSP', 'user', default=None)
mblighc31e4022008-12-11 19:32:30 +000074 if not user:
mblighdb59e3c2009-11-21 01:45:18 +000075 user = getpass.getuser()
mbligh451ede12009-02-12 21:54:03 +000076 if not server:
mbligh475f7762009-01-30 00:34:04 +000077 if 'AUTOTEST_WEB' in os.environ:
mbligh451ede12009-02-12 21:54:03 +000078 server = os.environ['AUTOTEST_WEB']
mbligh475f7762009-01-30 00:34:04 +000079 else:
mbligh451ede12009-02-12 21:54:03 +000080 server = GLOBAL_CONFIG.get_config_value('SERVER', 'hostname',
81 default=DEFAULT_SERVER)
82 self.server = server
mbligh67647152008-11-19 00:18:14 +000083 self.user = user
84 self.print_log = print_log
85 self.debug = debug
mbligh99b24f42009-06-08 16:45:55 +000086 self.reply_debug = reply_debug
Scott Zawalski347aaf42012-04-03 16:33:00 -040087 headers = {'AUTHORIZATION': self.user}
88 rpc_server = 'http://' + server + path
mbligh1354c9d2008-12-22 14:56:13 +000089 if debug:
90 print 'SERVER: %s' % rpc_server
91 print 'HEADERS: %s' % headers
mbligh67647152008-11-19 00:18:14 +000092 self.proxy = rpc_client_lib.get_proxy(rpc_server, headers=headers)
93
94
95 def run(self, call, **dargs):
96 """
97 Make a RPC call to the AFE server
98 """
99 rpc_call = getattr(self.proxy, call)
100 if self.debug:
101 print 'DEBUG: %s %s' % (call, dargs)
mbligh451ede12009-02-12 21:54:03 +0000102 try:
mbligh99b24f42009-06-08 16:45:55 +0000103 result = utils.strip_unicode(rpc_call(**dargs))
104 if self.reply_debug:
105 print result
106 return result
mbligh451ede12009-02-12 21:54:03 +0000107 except Exception:
mbligh451ede12009-02-12 21:54:03 +0000108 raise
mbligh67647152008-11-19 00:18:14 +0000109
110
111 def log(self, message):
112 if self.print_log:
113 print message
114
115
jamesrenc3940222010-02-19 21:57:37 +0000116class Planner(RpcClient):
117 def __init__(self, user=None, server=None, print_log=True, debug=False,
118 reply_debug=False):
119 super(Planner, self).__init__(path='/planner/server/rpc/',
120 user=user,
121 server=server,
122 print_log=print_log,
123 debug=debug,
124 reply_debug=reply_debug)
125
126
mbligh5280e3b2008-12-22 14:39:28 +0000127class TKO(RpcClient):
mbligh99b24f42009-06-08 16:45:55 +0000128 def __init__(self, user=None, server=None, print_log=True, debug=False,
129 reply_debug=False):
Scott Zawalski347aaf42012-04-03 16:33:00 -0400130 super(TKO, self).__init__(path='/new_tko/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000131 user=user,
132 server=server,
133 print_log=print_log,
134 debug=debug,
135 reply_debug=reply_debug)
Scott Zawalski63470dd2012-09-05 00:49:43 -0400136 self._db = None
137
138
Allen Li327e6fd2016-11-22 13:45:41 -0800139 @metrics.SecondsTimerDecorator(
140 '/chrome/infra/chromeos/autotest/tko/get_job_status_duration')
Scott Zawalski63470dd2012-09-05 00:49:43 -0400141 def get_job_test_statuses_from_db(self, job_id):
142 """Get job test statuses from the database.
143
144 Retrieve a set of fields from a job that reflect the status of each test
145 run within a job.
146 fields retrieved: status, test_name, reason, test_started_time,
147 test_finished_time, afe_job_id, job_owner, hostname.
148
149 @param job_id: The afe job id to look up.
150 @returns a TestStatus object of the resulting information.
151 """
152 if self._db is None:
Dan Shie8e0c052015-09-01 00:27:27 -0700153 self._db = db.db()
Fang Deng5c508332014-03-19 10:26:00 -0700154 fields = ['status', 'test_name', 'subdir', 'reason',
155 'test_started_time', 'test_finished_time', 'afe_job_id',
156 'job_owner', 'hostname', 'job_tag']
Scott Zawalski63470dd2012-09-05 00:49:43 -0400157 table = 'tko_test_view_2'
158 where = 'job_tag like "%s-%%"' % job_id
159 test_status = []
160 # Run commit before we query to ensure that we are pulling the latest
161 # results.
162 self._db.commit()
163 for entry in self._db.select(','.join(fields), table, (where, None)):
164 status_dict = {}
165 for key,value in zip(fields, entry):
166 # All callers expect values to be a str object.
167 status_dict[key] = str(value)
168 # id is used by TestStatus to uniquely identify each Test Status
169 # obj.
170 status_dict['id'] = [status_dict['reason'], status_dict['hostname'],
171 status_dict['test_name']]
172 test_status.append(status_dict)
173
174 return [TestStatus(self, e) for e in test_status]
mblighc31e4022008-12-11 19:32:30 +0000175
176
177 def get_status_counts(self, job, **data):
178 entries = self.run('get_status_counts',
mbligh1ef218d2009-08-03 16:57:56 +0000179 group_by=['hostname', 'test_name', 'reason'],
mblighc31e4022008-12-11 19:32:30 +0000180 job_tag__startswith='%s-' % job, **data)
mbligh5280e3b2008-12-22 14:39:28 +0000181 return [TestStatus(self, e) for e in entries['groups']]
mblighc31e4022008-12-11 19:32:30 +0000182
183
Richard Barnette260cbd02016-10-06 12:23:28 -0700184class _StableVersionMap(object):
185 """
186 A mapping from board names to strings naming software versions.
187
188 The mapping is meant to allow finding a nominally "stable" version
189 of software associated with a given board. The mapping identifies
190 specific versions of software that should be installed during
191 operations such as repair.
192
193 Conceptually, there are multiple version maps, each handling
194 different types of image. For instance, a single board may have
195 both a stable OS image (e.g. for CrOS), and a separate stable
196 firmware image.
197
198 Each different type of image requires a certain amount of special
199 handling, implemented by a subclass of `StableVersionMap`. The
200 subclasses take care of pre-processing of arguments, delegating
201 actual RPC calls to this superclass.
202
203 @property _afe AFE object through which to make the actual RPC
204 calls.
205 @property _android Value of the `android` parameter to be passed
206 when calling the `get_stable_version` RPC.
207 """
208
209 # DEFAULT_BOARD - The stable_version RPC API recognizes this special
210 # name as a mapping to use when no specific mapping for a board is
211 # present. This default mapping is only allowed for CrOS image
212 # types; other image type subclasses exclude it.
213 #
214 # TODO(jrbarnette): This value is copied from
215 # site_utils.stable_version_utils, because if we import that
216 # module here, it breaks unit tests. Something about the Django
217 # setup...
218 DEFAULT_BOARD = 'DEFAULT'
219
220
221 def __init__(self, afe, android):
222 self._afe = afe
223 self._android = android
224
225
226 def get_all_versions(self):
227 """
228 Get all mappings in the stable versions table.
229
230 Extracts the full content of the `stable_version` table
231 in the AFE database, and returns it as a dictionary
232 mapping board names to version strings.
233
234 @return A dictionary mapping board names to version strings.
235 """
236 return self._afe.run('get_all_stable_versions')
237
238
239 def get_version(self, board):
240 """
241 Get the mapping of one board in the stable versions table.
242
243 Look up and return the version mapped to the given board in the
244 `stable_versions` table in the AFE database.
245
246 @param board The board to be looked up.
247
248 @return The version mapped for the given board.
249 """
250 return self._afe.run('get_stable_version',
251 board=board, android=self._android)
252
253
254 def set_version(self, board, version):
255 """
256 Change the mapping of one board in the stable versions table.
257
258 Set the mapping in the `stable_versions` table in the AFE
259 database for the given board to the given version.
260
261 @param board The board to be updated.
262 @param version The new version to be assigned to the board.
263 """
264 self._afe.run('set_stable_version',
265 version=version, board=board)
266
267
268 def delete_version(self, board):
269 """
270 Remove the mapping of one board in the stable versions table.
271
272 Remove the mapping in the `stable_versions` table in the AFE
273 database for the given board.
274
275 @param board The board to be updated.
276 """
277 self._afe.run('delete_stable_version', board=board)
278
279
280class _OSVersionMap(_StableVersionMap):
281 """
282 Abstract stable version mapping for full OS images of various types.
283 """
284
285 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700286 # TODO(jrbarnette): We exclude non-OS (i.e. firmware) version
287 # mappings, but the returned dict doesn't distinguish CrOS
288 # boards from Android boards; both will be present, and the
289 # subclass can't distinguish them.
Richard Barnette260cbd02016-10-06 12:23:28 -0700290 #
291 # Ultimately, the right fix is to move knowledge of image type
292 # over to the RPC server side.
293 #
294 versions = super(_OSVersionMap, self).get_all_versions()
295 for board in versions.keys():
296 if '/' in board:
297 del versions[board]
298 return versions
299
300
301class _CrosVersionMap(_OSVersionMap):
302 """
303 Stable version mapping for Chrome OS release images.
304
305 This class manages a mapping of Chrome OS board names to known-good
306 release (or canary) images. The images selected can be installed on
307 DUTs during repair tasks, as a way of getting a DUT into a known
308 working state.
309 """
310
311 def __init__(self, afe):
312 super(_CrosVersionMap, self).__init__(afe, False)
313
Richard Barnette728e36f2016-11-03 16:04:29 -0700314 @staticmethod
315 def format_image_name(board, version):
316 """
317 Return an image name for a given `board` and `version`.
318
319 This formats `board` and `version` into a string identifying an
320 image file. The string represents part of a URL for access to
321 the image.
322
323 The returned image name is typically of a form like
324 "falco-release/R55-8872.44.0".
325 """
326 build_pattern = GLOBAL_CONFIG.get_config_value(
327 'CROS', 'stable_build_pattern')
328 return build_pattern % (board, version)
Richard Barnette260cbd02016-10-06 12:23:28 -0700329
Richard Barnette383ef9c2016-12-13 11:56:49 -0800330 def get_image_name(self, board):
331 """
332 Return the full image name of the stable version for `board`.
333
334 This finds the stable version for `board`, and returns a string
Richard Barnette728e36f2016-11-03 16:04:29 -0700335 identifying the associated image as for `format_image_name()`,
336 above.
Richard Barnette383ef9c2016-12-13 11:56:49 -0800337
338 @return A string identifying the image file for the stable
339 image for `board`.
340 """
Richard Barnette728e36f2016-11-03 16:04:29 -0700341 return self.format_image_name(board, self.get_version(board))
Richard Barnette260cbd02016-10-06 12:23:28 -0700342
343
344class _AndroidVersionMap(_OSVersionMap):
345 """
346 Stable version mapping for Android release images.
347
348 This class manages a mapping of Android/Brillo board names to
349 known-good images.
350 """
351
352 def __init__(self, afe):
353 super(_AndroidVersionMap, self).__init__(afe, True)
354
355
356 def get_all_versions(self):
357 versions = super(_AndroidVersionMap, self).get_all_versions()
358 del versions[self.DEFAULT_BOARD]
359 return versions
360
361
Richard Barnettee50453e2016-10-10 16:43:44 -0700362class _SuffixHackVersionMap(_StableVersionMap):
Richard Barnette260cbd02016-10-06 12:23:28 -0700363 """
Richard Barnettee50453e2016-10-10 16:43:44 -0700364 Abstract super class for mappings using a pseudo-board name.
Richard Barnette260cbd02016-10-06 12:23:28 -0700365
Richard Barnettee50453e2016-10-10 16:43:44 -0700366 For non-OS image type mappings, we look them up in the
367 `stable_versions` table by constructing a "pseudo-board" from the
368 real board name plus a suffix string that identifies the image type.
369 So, for instance the name "lulu/firmware" is used to look up the
370 FAFT firmware version for lulu boards.
Richard Barnette260cbd02016-10-06 12:23:28 -0700371 """
372
Richard Barnettee50453e2016-10-10 16:43:44 -0700373 # _SUFFIX - The suffix used in constructing the "pseudo-board"
374 # lookup key. Each subclass must define this value for itself.
Richard Barnette260cbd02016-10-06 12:23:28 -0700375 #
Richard Barnettee50453e2016-10-10 16:43:44 -0700376 _SUFFIX = None
Richard Barnette260cbd02016-10-06 12:23:28 -0700377
378 def __init__(self, afe):
Richard Barnettee50453e2016-10-10 16:43:44 -0700379 super(_SuffixHackVersionMap, self).__init__(afe, False)
Richard Barnette260cbd02016-10-06 12:23:28 -0700380
381
382 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700383 # Get all the mappings from the AFE, extract just the mappings
384 # with our suffix, and replace the pseudo-board name keys with
Richard Barnette260cbd02016-10-06 12:23:28 -0700385 # the real board names.
386 #
387 all_versions = super(
Richard Barnettee50453e2016-10-10 16:43:44 -0700388 _SuffixHackVersionMap, self).get_all_versions()
Richard Barnette260cbd02016-10-06 12:23:28 -0700389 return {
390 board[0 : -len(self._SUFFIX)]: all_versions[board]
391 for board in all_versions.keys()
392 if board.endswith(self._SUFFIX)
393 }
394
395
396 def get_version(self, board):
Richard Barnettee50453e2016-10-10 16:43:44 -0700397 board += self._SUFFIX
398 return super(_SuffixHackVersionMap, self).get_version(board)
399
400
401 def set_version(self, board, version):
402 board += self._SUFFIX
403 super(_SuffixHackVersionMap, self).set_version(board, version)
404
405
406 def delete_version(self, board):
407 board += self._SUFFIX
408 super(_SuffixHackVersionMap, self).delete_version(board)
409
410
411class _FAFTVersionMap(_SuffixHackVersionMap):
412 """
413 Stable version mapping for firmware versions used in FAFT repair.
414
415 When DUTs used for FAFT fail repair, stable firmware may need to be
416 flashed directly from original tarballs. The FAFT firmware version
417 mapping finds the appropriate tarball for a given board.
418 """
419
420 _SUFFIX = '/firmware'
421
422 def get_version(self, board):
423 # If there's no mapping for `board`, the lookup will return the
424 # default CrOS version mapping. To eliminate that case, we
425 # require a '/' character in the version, since CrOS versions
426 # won't match that.
Richard Barnette260cbd02016-10-06 12:23:28 -0700427 #
428 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
429 # the right fix is to move handling to the RPC server side.
430 #
Richard Barnette260cbd02016-10-06 12:23:28 -0700431 version = super(_FAFTVersionMap, self).get_version(board)
432 return version if '/' in version else None
433
434
Richard Barnettee50453e2016-10-10 16:43:44 -0700435class _FirmwareVersionMap(_SuffixHackVersionMap):
436 """
437 Stable version mapping for firmware supplied in Chrome OS images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700438
Richard Barnettee50453e2016-10-10 16:43:44 -0700439 A Chrome OS image bundles a version of the firmware that the
440 device should update to when the OS version is installed during
441 AU.
Richard Barnette260cbd02016-10-06 12:23:28 -0700442
Richard Barnettee50453e2016-10-10 16:43:44 -0700443 Test images suppress the firmware update during AU. Instead, during
444 repair and verify we check installed firmware on a DUT, compare it
445 against the stable version mapping for the board, and update when
446 the DUT is out-of-date.
447 """
448
449 _SUFFIX = '/rwfw'
450
451 def get_version(self, board):
452 # If there's no mapping for `board`, the lookup will return the
453 # default CrOS version mapping. To eliminate that case, we
454 # require the version start with "Google_", since CrOS versions
455 # won't match that.
456 #
457 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
458 # the right fix is to move handling to the RPC server side.
459 #
460 version = super(_FirmwareVersionMap, self).get_version(board)
461 return version if version.startswith('Google_') else None
Richard Barnette260cbd02016-10-06 12:23:28 -0700462
463
mbligh5280e3b2008-12-22 14:39:28 +0000464class AFE(RpcClient):
mbligh17c75e62009-06-08 16:18:21 +0000465 def __init__(self, user=None, server=None, print_log=True, debug=False,
mbligh99b24f42009-06-08 16:45:55 +0000466 reply_debug=False, job=None):
mbligh17c75e62009-06-08 16:18:21 +0000467 self.job = job
Scott Zawalski347aaf42012-04-03 16:33:00 -0400468 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000469 user=user,
470 server=server,
471 print_log=print_log,
472 debug=debug,
473 reply_debug=reply_debug)
mblighc31e4022008-12-11 19:32:30 +0000474
mbligh1ef218d2009-08-03 16:57:56 +0000475
Richard Barnette260cbd02016-10-06 12:23:28 -0700476 # Known image types for stable version mapping objects.
477 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
478 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
Richard Barnettee50453e2016-10-10 16:43:44 -0700479 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700480 # ANDROID_IMAGE_TYPE - Mappings for Android images.
481 #
482 CROS_IMAGE_TYPE = 'cros'
483 FAFT_IMAGE_TYPE = 'faft'
Richard Barnettee50453e2016-10-10 16:43:44 -0700484 FIRMWARE_IMAGE_TYPE = 'firmware'
Richard Barnette260cbd02016-10-06 12:23:28 -0700485 ANDROID_IMAGE_TYPE = 'android'
486
487 _IMAGE_MAPPING_CLASSES = {
488 CROS_IMAGE_TYPE: _CrosVersionMap,
489 FAFT_IMAGE_TYPE: _FAFTVersionMap,
Richard Barnettee50453e2016-10-10 16:43:44 -0700490 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
Richard Barnette260cbd02016-10-06 12:23:28 -0700491 ANDROID_IMAGE_TYPE: _AndroidVersionMap
492 }
493
494
495 def get_stable_version_map(self, image_type):
496 """
497 Return a stable version mapping for the given image type.
498
499 @return An object mapping board names to version strings for
500 software of the given image type.
501 """
502 return self._IMAGE_MAPPING_CLASSES[image_type](self)
503
504
mbligh67647152008-11-19 00:18:14 +0000505 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000506 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000507 statuses = self.run('get_static_data')['host_statuses']
508 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000509 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000510 if live == False:
511 return dead_statuses
512 else:
513 return statuses
514
515
mbligh71094012009-12-19 05:35:21 +0000516 @staticmethod
517 def _dict_for_host_query(hostnames=(), status=None, label=None):
518 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000519 if hostnames:
520 query_args['hostname__in'] = hostnames
521 if status:
522 query_args['status'] = status
523 if label:
524 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000525 return query_args
526
527
528 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
529 query_args = dict(dargs)
530 query_args.update(self._dict_for_host_query(hostnames=hostnames,
531 status=status,
532 label=label))
533 hosts = self.run('get_hosts', **query_args)
534 return [Host(self, h) for h in hosts]
535
536
537 def get_hostnames(self, status=None, label=None, **dargs):
538 """Like get_hosts() but returns hostnames instead of Host objects."""
539 # This implementation can be replaced with a more efficient one
540 # that does not query for entire host objects in the future.
541 return [host_obj.hostname for host_obj in
542 self.get_hosts(status=status, label=label, **dargs)]
543
544
545 def reverify_hosts(self, hostnames=(), status=None, label=None):
546 query_args = dict(locked=False,
547 aclgroup__users__login=self.user)
548 query_args.update(self._dict_for_host_query(hostnames=hostnames,
549 status=status,
550 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000551 return self.run('reverify_hosts', **query_args)
552
553
mbligh67647152008-11-19 00:18:14 +0000554 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000555 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000556 return self.get_hosts(id=id)[0]
557
558
MK Ryuacf35922014-10-03 14:56:49 -0700559 def get_host_attribute(self, attr, **dargs):
560 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
561 return [HostAttribute(self, a) for a in host_attrs]
562
563
Chris Masone8abb6fc2012-01-31 09:27:36 -0800564 def set_host_attribute(self, attr, val, **dargs):
565 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
566
567
mbligh67647152008-11-19 00:18:14 +0000568 def get_labels(self, **dargs):
569 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000570 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000571
572
573 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000574 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000575 return self.get_labels(id=id)[0]
576
577
578 def get_acls(self, **dargs):
579 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000580 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000581
582
583 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000584 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000585 return self.get_acls(id=id)[0]
586
587
mbligh54459c72009-01-21 19:26:44 +0000588 def get_users(self, **dargs):
589 users = self.run('get_users', **dargs)
590 return [User(self, u) for u in users]
591
592
mbligh1354c9d2008-12-22 14:56:13 +0000593 def generate_control_file(self, tests, **dargs):
594 ret = self.run('generate_control_file', tests=tests, **dargs)
595 return ControlFile(self, ret)
596
597
mbligh67647152008-11-19 00:18:14 +0000598 def get_jobs(self, summary=False, **dargs):
599 if summary:
600 jobs_data = self.run('get_jobs_summary', **dargs)
601 else:
602 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000603 jobs = []
604 for j in jobs_data:
605 job = Job(self, j)
606 # Set up some extra information defaults
607 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
608 job.platform_results = {}
609 job.platform_reasons = {}
610 jobs.append(job)
611 return jobs
mbligh67647152008-11-19 00:18:14 +0000612
613
614 def get_host_queue_entries(self, **data):
615 entries = self.run('get_host_queue_entries', **data)
mblighf9e35862009-02-26 01:03:11 +0000616 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000617
618 # Sadly, get_host_queue_entries doesn't return platforms, we have
619 # to get those back from an explicit get_hosts queury, then patch
620 # the new host objects back into the host list.
621 hostnames = [s.host.hostname for s in job_statuses if s.host]
622 host_hash = {}
623 for host in self.get_hosts(hostname__in=hostnames):
624 host_hash[host.hostname] = host
625 for status in job_statuses:
626 if status.host:
Fang Deng97dafbc2015-04-23 23:06:18 -0700627 status.host = host_hash.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000628 # filter job statuses that have either host or meta_host
629 return [status for status in job_statuses if (status.host or
630 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000631
632
MK Ryu1b2d7f92015-02-24 17:45:02 -0800633 def get_special_tasks(self, **data):
634 tasks = self.run('get_special_tasks', **data)
635 return [SpecialTask(self, t) for t in tasks]
636
637
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700638 def get_host_special_tasks(self, host_id, **data):
639 tasks = self.run('get_host_special_tasks',
640 host_id=host_id, **data)
641 return [SpecialTask(self, t) for t in tasks]
642
643
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700644 def get_host_status_task(self, host_id, end_time):
645 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700646 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700647 return SpecialTask(self, task) if task else None
648
649
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700650 def get_host_diagnosis_interval(self, host_id, end_time, success):
651 return self.run('get_host_diagnosis_interval',
652 host_id=host_id, end_time=end_time,
653 success=success)
654
655
mbligh67647152008-11-19 00:18:14 +0000656 def create_job(self, control_file, name=' ', priority='Medium',
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700657 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT, **dargs):
mbligh67647152008-11-19 00:18:14 +0000658 id = self.run('create_job', name=name, priority=priority,
659 control_file=control_file, control_type=control_type, **dargs)
660 return self.get_jobs(id=id)[0]
661
662
Simran Basi01984f52015-10-12 15:36:45 -0700663 def abort_jobs(self, jobs):
664 """Abort a list of jobs.
665
666 Already completed jobs will not be affected.
667
668 @param jobs: List of job ids to abort.
669 """
670 for job in jobs:
671 self.run('abort_host_queue_entries', job_id=job)
672
673
Kevin Cheng19521982016-09-22 12:27:23 -0700674 def get_hosts_by_attribute(self, attribute, value):
675 """
676 Get the list of hosts that share the same host attribute value.
677
678 @param attribute: String of the host attribute to check.
679 @param value: String of the value that is shared between hosts.
680
681 @returns List of hostnames that all have the same host attribute and
682 value.
683 """
684 return self.run('get_hosts_by_attribute',
685 attribute=attribute, value=value)
686
687
688 def lock_host(self, host, lock_reason, fail_if_locked=False):
689 """
690 Lock the given host with the given lock reason.
691
692 Locking a host that's already locked using the 'modify_hosts' rpc
693 will raise an exception. That's why fail_if_locked exists so the
694 caller can determine if the lock succeeded or failed. This will
695 save every caller from wrapping lock_host in a try-except.
696
697 @param host: hostname of host to lock.
698 @param lock_reason: Reason for locking host.
699 @param fail_if_locked: Return False if host is already locked.
700
701 @returns Boolean, True if lock was successful, False otherwise.
702 """
703 try:
704 self.run('modify_hosts',
705 host_filter_data={'hostname': host},
706 update_data={'locked': True,
707 'lock_reason': lock_reason})
708 except Exception:
709 return not fail_if_locked
710 return True
711
712
713 def unlock_hosts(self, locked_hosts):
714 """
715 Unlock the hosts.
716
717 Unlocking a host that's already unlocked will do nothing so we don't
718 need any special try-except clause here.
719
720 @param locked_hosts: List of hostnames of hosts to unlock.
721 """
722 self.run('modify_hosts',
723 host_filter_data={'hostname__in': locked_hosts},
724 update_data={'locked': False,
725 'lock_reason': ''})
726
727
mbligh5280e3b2008-12-22 14:39:28 +0000728class TestResults(object):
729 """
730 Container class used to hold the results of the tests for a job
731 """
732 def __init__(self):
733 self.good = []
734 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000735 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000736
737
738 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000739 if result.complete_count > result.pass_count:
740 self.fail.append(result)
741 elif result.incomplete_count > 0:
742 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000743 else:
mbligh451ede12009-02-12 21:54:03 +0000744 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000745
746
747class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000748 """
749 Generic object used to construct python objects from rpc calls
750 """
751 def __init__(self, afe, hash):
752 self.afe = afe
753 self.hash = hash
754 self.__dict__.update(hash)
755
756
757 def __str__(self):
758 return dump_object(self.__repr__(), self)
759
760
mbligh1354c9d2008-12-22 14:56:13 +0000761class ControlFile(RpcObject):
762 """
763 AFE control file object
764
765 Fields: synch_count, dependencies, control_file, is_server
766 """
767 def __repr__(self):
768 return 'CONTROL FILE: %s' % self.control_file
769
770
mbligh5280e3b2008-12-22 14:39:28 +0000771class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000772 """
773 AFE label object
774
775 Fields:
776 name, invalid, platform, kernel_config, id, only_if_needed
777 """
778 def __repr__(self):
779 return 'LABEL: %s' % self.name
780
781
782 def add_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800783 return self.afe.run('label_add_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000784
785
786 def remove_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800787 return self.afe.run('label_remove_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000788
789
mbligh5280e3b2008-12-22 14:39:28 +0000790class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000791 """
792 AFE acl object
793
794 Fields:
795 users, hosts, description, name, id
796 """
797 def __repr__(self):
798 return 'ACL: %s' % self.name
799
800
801 def add_hosts(self, hosts):
802 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
803 return self.afe.run('acl_group_add_hosts', self.id, hosts)
804
805
806 def remove_hosts(self, hosts):
807 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
808 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
809
810
mbligh54459c72009-01-21 19:26:44 +0000811 def add_users(self, users):
812 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
813 return self.afe.run('acl_group_add_users', id=self.name, users=users)
814
815
mbligh5280e3b2008-12-22 14:39:28 +0000816class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000817 """
818 AFE job object
819
820 Fields:
821 name, control_file, control_type, synch_count, reboot_before,
822 run_verify, priority, email_list, created_on, dependencies,
823 timeout, owner, reboot_after, id
824 """
825 def __repr__(self):
826 return 'JOB: %s' % self.id
827
828
mbligh5280e3b2008-12-22 14:39:28 +0000829class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000830 """
831 AFE job_status object
832
833 Fields:
834 status, complete, deleted, meta_host, host, active, execution_subdir, id
835 """
836 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800837 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000838 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700839 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000840 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000841
842
843 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000844 if self.host and self.host.hostname:
845 hostname = self.host.hostname
846 else:
847 hostname = 'None'
848 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000849
850
MK Ryu1b2d7f92015-02-24 17:45:02 -0800851class SpecialTask(RpcObject):
852 """
853 AFE special task object
854 """
855 def __init__(self, afe, hash):
856 super(SpecialTask, self).__init__(afe, hash)
857 self.host = Host(afe, self.host)
858
859
860 def __repr__(self):
861 return 'SPECIAL TASK: %s' % self.id
862
863
mbligh5280e3b2008-12-22 14:39:28 +0000864class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000865 """
866 AFE host object
867
868 Fields:
869 status, lock_time, locked_by, locked, hostname, invalid,
870 synch_id, labels, platform, protection, dirty, id
871 """
872 def __repr__(self):
873 return 'HOST OBJECT: %s' % self.hostname
874
875
876 def show(self):
877 labels = list(set(self.labels) - set([self.platform]))
878 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
879 self.locked, self.platform,
880 ', '.join(labels))
881
882
mbligh54459c72009-01-21 19:26:44 +0000883 def delete(self):
884 return self.afe.run('delete_host', id=self.id)
885
886
mbligh6463c4b2009-01-30 00:33:37 +0000887 def modify(self, **dargs):
888 return self.afe.run('modify_host', id=self.id, **dargs)
889
890
mbligh67647152008-11-19 00:18:14 +0000891 def get_acls(self):
892 return self.afe.get_acls(hosts__hostname=self.hostname)
893
894
895 def add_acl(self, acl_name):
896 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
897 return self.afe.run('acl_group_add_hosts', id=acl_name,
898 hosts=[self.hostname])
899
900
901 def remove_acl(self, acl_name):
902 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
903 return self.afe.run('acl_group_remove_hosts', id=acl_name,
904 hosts=[self.hostname])
905
906
907 def get_labels(self):
908 return self.afe.get_labels(host__hostname__in=[self.hostname])
909
910
911 def add_labels(self, labels):
912 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
913 return self.afe.run('host_add_labels', id=self.id, labels=labels)
914
915
916 def remove_labels(self, labels):
917 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
918 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000919
920
mbligh54459c72009-01-21 19:26:44 +0000921class User(RpcObject):
922 def __repr__(self):
923 return 'USER: %s' % self.login
924
925
mbligh5280e3b2008-12-22 14:39:28 +0000926class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000927 """
928 TKO test status object
929
930 Fields:
931 test_idx, hostname, testname, id
932 complete_count, incomplete_count, group_count, pass_count
933 """
934 def __repr__(self):
935 return 'TEST STATUS: %s' % self.id
936
937
MK Ryuacf35922014-10-03 14:56:49 -0700938class HostAttribute(RpcObject):
939 """
940 AFE host attribute object
941
942 Fields:
943 id, host, attribute, value
944 """
945 def __repr__(self):
946 return 'HOST ATTRIBUTE %d' % self.id