blob: c9f0e0ee4e144293f7d8d3a9ff3cdd8d75b68e9f [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
314
315 def get_version(self, board):
316 version = super(_CrosVersionMap, self).get_version(board)
317 build_pattern = GLOBAL_CONFIG.get_config_value(
318 'CROS', 'stable_build_pattern')
319 return build_pattern % (board, version)
320
321
322class _AndroidVersionMap(_OSVersionMap):
323 """
324 Stable version mapping for Android release images.
325
326 This class manages a mapping of Android/Brillo board names to
327 known-good images.
328 """
329
330 def __init__(self, afe):
331 super(_AndroidVersionMap, self).__init__(afe, True)
332
333
334 def get_all_versions(self):
335 versions = super(_AndroidVersionMap, self).get_all_versions()
336 del versions[self.DEFAULT_BOARD]
337 return versions
338
339
Richard Barnettee50453e2016-10-10 16:43:44 -0700340class _SuffixHackVersionMap(_StableVersionMap):
Richard Barnette260cbd02016-10-06 12:23:28 -0700341 """
Richard Barnettee50453e2016-10-10 16:43:44 -0700342 Abstract super class for mappings using a pseudo-board name.
Richard Barnette260cbd02016-10-06 12:23:28 -0700343
Richard Barnettee50453e2016-10-10 16:43:44 -0700344 For non-OS image type mappings, we look them up in the
345 `stable_versions` table by constructing a "pseudo-board" from the
346 real board name plus a suffix string that identifies the image type.
347 So, for instance the name "lulu/firmware" is used to look up the
348 FAFT firmware version for lulu boards.
Richard Barnette260cbd02016-10-06 12:23:28 -0700349 """
350
Richard Barnettee50453e2016-10-10 16:43:44 -0700351 # _SUFFIX - The suffix used in constructing the "pseudo-board"
352 # lookup key. Each subclass must define this value for itself.
Richard Barnette260cbd02016-10-06 12:23:28 -0700353 #
Richard Barnettee50453e2016-10-10 16:43:44 -0700354 _SUFFIX = None
Richard Barnette260cbd02016-10-06 12:23:28 -0700355
356 def __init__(self, afe):
Richard Barnettee50453e2016-10-10 16:43:44 -0700357 super(_SuffixHackVersionMap, self).__init__(afe, False)
Richard Barnette260cbd02016-10-06 12:23:28 -0700358
359
360 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700361 # Get all the mappings from the AFE, extract just the mappings
362 # with our suffix, and replace the pseudo-board name keys with
Richard Barnette260cbd02016-10-06 12:23:28 -0700363 # the real board names.
364 #
365 all_versions = super(
Richard Barnettee50453e2016-10-10 16:43:44 -0700366 _SuffixHackVersionMap, self).get_all_versions()
Richard Barnette260cbd02016-10-06 12:23:28 -0700367 return {
368 board[0 : -len(self._SUFFIX)]: all_versions[board]
369 for board in all_versions.keys()
370 if board.endswith(self._SUFFIX)
371 }
372
373
374 def get_version(self, board):
Richard Barnettee50453e2016-10-10 16:43:44 -0700375 board += self._SUFFIX
376 return super(_SuffixHackVersionMap, self).get_version(board)
377
378
379 def set_version(self, board, version):
380 board += self._SUFFIX
381 super(_SuffixHackVersionMap, self).set_version(board, version)
382
383
384 def delete_version(self, board):
385 board += self._SUFFIX
386 super(_SuffixHackVersionMap, self).delete_version(board)
387
388
389class _FAFTVersionMap(_SuffixHackVersionMap):
390 """
391 Stable version mapping for firmware versions used in FAFT repair.
392
393 When DUTs used for FAFT fail repair, stable firmware may need to be
394 flashed directly from original tarballs. The FAFT firmware version
395 mapping finds the appropriate tarball for a given board.
396 """
397
398 _SUFFIX = '/firmware'
399
400 def get_version(self, board):
401 # If there's no mapping for `board`, the lookup will return the
402 # default CrOS version mapping. To eliminate that case, we
403 # require a '/' character in the version, since CrOS versions
404 # won't match that.
Richard Barnette260cbd02016-10-06 12:23:28 -0700405 #
406 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
407 # the right fix is to move handling to the RPC server side.
408 #
Richard Barnette260cbd02016-10-06 12:23:28 -0700409 version = super(_FAFTVersionMap, self).get_version(board)
410 return version if '/' in version else None
411
412
Richard Barnettee50453e2016-10-10 16:43:44 -0700413class _FirmwareVersionMap(_SuffixHackVersionMap):
414 """
415 Stable version mapping for firmware supplied in Chrome OS images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700416
Richard Barnettee50453e2016-10-10 16:43:44 -0700417 A Chrome OS image bundles a version of the firmware that the
418 device should update to when the OS version is installed during
419 AU.
Richard Barnette260cbd02016-10-06 12:23:28 -0700420
Richard Barnettee50453e2016-10-10 16:43:44 -0700421 Test images suppress the firmware update during AU. Instead, during
422 repair and verify we check installed firmware on a DUT, compare it
423 against the stable version mapping for the board, and update when
424 the DUT is out-of-date.
425 """
426
427 _SUFFIX = '/rwfw'
428
429 def get_version(self, board):
430 # If there's no mapping for `board`, the lookup will return the
431 # default CrOS version mapping. To eliminate that case, we
432 # require the version start with "Google_", since CrOS versions
433 # won't match that.
434 #
435 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
436 # the right fix is to move handling to the RPC server side.
437 #
438 version = super(_FirmwareVersionMap, self).get_version(board)
439 return version if version.startswith('Google_') else None
Richard Barnette260cbd02016-10-06 12:23:28 -0700440
441
mbligh5280e3b2008-12-22 14:39:28 +0000442class AFE(RpcClient):
mbligh17c75e62009-06-08 16:18:21 +0000443 def __init__(self, user=None, server=None, print_log=True, debug=False,
mbligh99b24f42009-06-08 16:45:55 +0000444 reply_debug=False, job=None):
mbligh17c75e62009-06-08 16:18:21 +0000445 self.job = job
Scott Zawalski347aaf42012-04-03 16:33:00 -0400446 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000447 user=user,
448 server=server,
449 print_log=print_log,
450 debug=debug,
451 reply_debug=reply_debug)
mblighc31e4022008-12-11 19:32:30 +0000452
mbligh1ef218d2009-08-03 16:57:56 +0000453
Richard Barnette260cbd02016-10-06 12:23:28 -0700454 # Known image types for stable version mapping objects.
455 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
456 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
Richard Barnettee50453e2016-10-10 16:43:44 -0700457 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700458 # ANDROID_IMAGE_TYPE - Mappings for Android images.
459 #
460 CROS_IMAGE_TYPE = 'cros'
461 FAFT_IMAGE_TYPE = 'faft'
Richard Barnettee50453e2016-10-10 16:43:44 -0700462 FIRMWARE_IMAGE_TYPE = 'firmware'
Richard Barnette260cbd02016-10-06 12:23:28 -0700463 ANDROID_IMAGE_TYPE = 'android'
464
465 _IMAGE_MAPPING_CLASSES = {
466 CROS_IMAGE_TYPE: _CrosVersionMap,
467 FAFT_IMAGE_TYPE: _FAFTVersionMap,
Richard Barnettee50453e2016-10-10 16:43:44 -0700468 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
Richard Barnette260cbd02016-10-06 12:23:28 -0700469 ANDROID_IMAGE_TYPE: _AndroidVersionMap
470 }
471
472
473 def get_stable_version_map(self, image_type):
474 """
475 Return a stable version mapping for the given image type.
476
477 @return An object mapping board names to version strings for
478 software of the given image type.
479 """
480 return self._IMAGE_MAPPING_CLASSES[image_type](self)
481
482
mbligh67647152008-11-19 00:18:14 +0000483 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000484 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000485 statuses = self.run('get_static_data')['host_statuses']
486 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000487 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000488 if live == False:
489 return dead_statuses
490 else:
491 return statuses
492
493
mbligh71094012009-12-19 05:35:21 +0000494 @staticmethod
495 def _dict_for_host_query(hostnames=(), status=None, label=None):
496 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000497 if hostnames:
498 query_args['hostname__in'] = hostnames
499 if status:
500 query_args['status'] = status
501 if label:
502 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000503 return query_args
504
505
506 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
507 query_args = dict(dargs)
508 query_args.update(self._dict_for_host_query(hostnames=hostnames,
509 status=status,
510 label=label))
511 hosts = self.run('get_hosts', **query_args)
512 return [Host(self, h) for h in hosts]
513
514
515 def get_hostnames(self, status=None, label=None, **dargs):
516 """Like get_hosts() but returns hostnames instead of Host objects."""
517 # This implementation can be replaced with a more efficient one
518 # that does not query for entire host objects in the future.
519 return [host_obj.hostname for host_obj in
520 self.get_hosts(status=status, label=label, **dargs)]
521
522
523 def reverify_hosts(self, hostnames=(), status=None, label=None):
524 query_args = dict(locked=False,
525 aclgroup__users__login=self.user)
526 query_args.update(self._dict_for_host_query(hostnames=hostnames,
527 status=status,
528 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000529 return self.run('reverify_hosts', **query_args)
530
531
mbligh67647152008-11-19 00:18:14 +0000532 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000533 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000534 return self.get_hosts(id=id)[0]
535
536
MK Ryuacf35922014-10-03 14:56:49 -0700537 def get_host_attribute(self, attr, **dargs):
538 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
539 return [HostAttribute(self, a) for a in host_attrs]
540
541
Chris Masone8abb6fc2012-01-31 09:27:36 -0800542 def set_host_attribute(self, attr, val, **dargs):
543 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
544
545
mbligh67647152008-11-19 00:18:14 +0000546 def get_labels(self, **dargs):
547 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000548 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000549
550
551 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000552 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000553 return self.get_labels(id=id)[0]
554
555
556 def get_acls(self, **dargs):
557 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000558 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000559
560
561 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000562 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000563 return self.get_acls(id=id)[0]
564
565
mbligh54459c72009-01-21 19:26:44 +0000566 def get_users(self, **dargs):
567 users = self.run('get_users', **dargs)
568 return [User(self, u) for u in users]
569
570
mbligh1354c9d2008-12-22 14:56:13 +0000571 def generate_control_file(self, tests, **dargs):
572 ret = self.run('generate_control_file', tests=tests, **dargs)
573 return ControlFile(self, ret)
574
575
mbligh67647152008-11-19 00:18:14 +0000576 def get_jobs(self, summary=False, **dargs):
577 if summary:
578 jobs_data = self.run('get_jobs_summary', **dargs)
579 else:
580 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000581 jobs = []
582 for j in jobs_data:
583 job = Job(self, j)
584 # Set up some extra information defaults
585 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
586 job.platform_results = {}
587 job.platform_reasons = {}
588 jobs.append(job)
589 return jobs
mbligh67647152008-11-19 00:18:14 +0000590
591
592 def get_host_queue_entries(self, **data):
593 entries = self.run('get_host_queue_entries', **data)
mblighf9e35862009-02-26 01:03:11 +0000594 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000595
596 # Sadly, get_host_queue_entries doesn't return platforms, we have
597 # to get those back from an explicit get_hosts queury, then patch
598 # the new host objects back into the host list.
599 hostnames = [s.host.hostname for s in job_statuses if s.host]
600 host_hash = {}
601 for host in self.get_hosts(hostname__in=hostnames):
602 host_hash[host.hostname] = host
603 for status in job_statuses:
604 if status.host:
Fang Deng97dafbc2015-04-23 23:06:18 -0700605 status.host = host_hash.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000606 # filter job statuses that have either host or meta_host
607 return [status for status in job_statuses if (status.host or
608 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000609
610
MK Ryu1b2d7f92015-02-24 17:45:02 -0800611 def get_special_tasks(self, **data):
612 tasks = self.run('get_special_tasks', **data)
613 return [SpecialTask(self, t) for t in tasks]
614
615
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700616 def get_host_special_tasks(self, host_id, **data):
617 tasks = self.run('get_host_special_tasks',
618 host_id=host_id, **data)
619 return [SpecialTask(self, t) for t in tasks]
620
621
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700622 def get_host_status_task(self, host_id, end_time):
623 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700624 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700625 return SpecialTask(self, task) if task else None
626
627
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700628 def get_host_diagnosis_interval(self, host_id, end_time, success):
629 return self.run('get_host_diagnosis_interval',
630 host_id=host_id, end_time=end_time,
631 success=success)
632
633
mbligh67647152008-11-19 00:18:14 +0000634 def create_job(self, control_file, name=' ', priority='Medium',
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700635 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT, **dargs):
mbligh67647152008-11-19 00:18:14 +0000636 id = self.run('create_job', name=name, priority=priority,
637 control_file=control_file, control_type=control_type, **dargs)
638 return self.get_jobs(id=id)[0]
639
640
Simran Basi01984f52015-10-12 15:36:45 -0700641 def abort_jobs(self, jobs):
642 """Abort a list of jobs.
643
644 Already completed jobs will not be affected.
645
646 @param jobs: List of job ids to abort.
647 """
648 for job in jobs:
649 self.run('abort_host_queue_entries', job_id=job)
650
651
Kevin Cheng19521982016-09-22 12:27:23 -0700652 def get_hosts_by_attribute(self, attribute, value):
653 """
654 Get the list of hosts that share the same host attribute value.
655
656 @param attribute: String of the host attribute to check.
657 @param value: String of the value that is shared between hosts.
658
659 @returns List of hostnames that all have the same host attribute and
660 value.
661 """
662 return self.run('get_hosts_by_attribute',
663 attribute=attribute, value=value)
664
665
666 def lock_host(self, host, lock_reason, fail_if_locked=False):
667 """
668 Lock the given host with the given lock reason.
669
670 Locking a host that's already locked using the 'modify_hosts' rpc
671 will raise an exception. That's why fail_if_locked exists so the
672 caller can determine if the lock succeeded or failed. This will
673 save every caller from wrapping lock_host in a try-except.
674
675 @param host: hostname of host to lock.
676 @param lock_reason: Reason for locking host.
677 @param fail_if_locked: Return False if host is already locked.
678
679 @returns Boolean, True if lock was successful, False otherwise.
680 """
681 try:
682 self.run('modify_hosts',
683 host_filter_data={'hostname': host},
684 update_data={'locked': True,
685 'lock_reason': lock_reason})
686 except Exception:
687 return not fail_if_locked
688 return True
689
690
691 def unlock_hosts(self, locked_hosts):
692 """
693 Unlock the hosts.
694
695 Unlocking a host that's already unlocked will do nothing so we don't
696 need any special try-except clause here.
697
698 @param locked_hosts: List of hostnames of hosts to unlock.
699 """
700 self.run('modify_hosts',
701 host_filter_data={'hostname__in': locked_hosts},
702 update_data={'locked': False,
703 'lock_reason': ''})
704
705
mbligh5280e3b2008-12-22 14:39:28 +0000706class TestResults(object):
707 """
708 Container class used to hold the results of the tests for a job
709 """
710 def __init__(self):
711 self.good = []
712 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000713 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000714
715
716 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000717 if result.complete_count > result.pass_count:
718 self.fail.append(result)
719 elif result.incomplete_count > 0:
720 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000721 else:
mbligh451ede12009-02-12 21:54:03 +0000722 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000723
724
725class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000726 """
727 Generic object used to construct python objects from rpc calls
728 """
729 def __init__(self, afe, hash):
730 self.afe = afe
731 self.hash = hash
732 self.__dict__.update(hash)
733
734
735 def __str__(self):
736 return dump_object(self.__repr__(), self)
737
738
mbligh1354c9d2008-12-22 14:56:13 +0000739class ControlFile(RpcObject):
740 """
741 AFE control file object
742
743 Fields: synch_count, dependencies, control_file, is_server
744 """
745 def __repr__(self):
746 return 'CONTROL FILE: %s' % self.control_file
747
748
mbligh5280e3b2008-12-22 14:39:28 +0000749class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000750 """
751 AFE label object
752
753 Fields:
754 name, invalid, platform, kernel_config, id, only_if_needed
755 """
756 def __repr__(self):
757 return 'LABEL: %s' % self.name
758
759
760 def add_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800761 return self.afe.run('label_add_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000762
763
764 def remove_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800765 return self.afe.run('label_remove_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000766
767
mbligh5280e3b2008-12-22 14:39:28 +0000768class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000769 """
770 AFE acl object
771
772 Fields:
773 users, hosts, description, name, id
774 """
775 def __repr__(self):
776 return 'ACL: %s' % self.name
777
778
779 def add_hosts(self, hosts):
780 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
781 return self.afe.run('acl_group_add_hosts', self.id, hosts)
782
783
784 def remove_hosts(self, hosts):
785 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
786 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
787
788
mbligh54459c72009-01-21 19:26:44 +0000789 def add_users(self, users):
790 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
791 return self.afe.run('acl_group_add_users', id=self.name, users=users)
792
793
mbligh5280e3b2008-12-22 14:39:28 +0000794class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000795 """
796 AFE job object
797
798 Fields:
799 name, control_file, control_type, synch_count, reboot_before,
800 run_verify, priority, email_list, created_on, dependencies,
801 timeout, owner, reboot_after, id
802 """
803 def __repr__(self):
804 return 'JOB: %s' % self.id
805
806
mbligh5280e3b2008-12-22 14:39:28 +0000807class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000808 """
809 AFE job_status object
810
811 Fields:
812 status, complete, deleted, meta_host, host, active, execution_subdir, id
813 """
814 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800815 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000816 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700817 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000818 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000819
820
821 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000822 if self.host and self.host.hostname:
823 hostname = self.host.hostname
824 else:
825 hostname = 'None'
826 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000827
828
MK Ryu1b2d7f92015-02-24 17:45:02 -0800829class SpecialTask(RpcObject):
830 """
831 AFE special task object
832 """
833 def __init__(self, afe, hash):
834 super(SpecialTask, self).__init__(afe, hash)
835 self.host = Host(afe, self.host)
836
837
838 def __repr__(self):
839 return 'SPECIAL TASK: %s' % self.id
840
841
mbligh5280e3b2008-12-22 14:39:28 +0000842class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000843 """
844 AFE host object
845
846 Fields:
847 status, lock_time, locked_by, locked, hostname, invalid,
848 synch_id, labels, platform, protection, dirty, id
849 """
850 def __repr__(self):
851 return 'HOST OBJECT: %s' % self.hostname
852
853
854 def show(self):
855 labels = list(set(self.labels) - set([self.platform]))
856 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
857 self.locked, self.platform,
858 ', '.join(labels))
859
860
mbligh54459c72009-01-21 19:26:44 +0000861 def delete(self):
862 return self.afe.run('delete_host', id=self.id)
863
864
mbligh6463c4b2009-01-30 00:33:37 +0000865 def modify(self, **dargs):
866 return self.afe.run('modify_host', id=self.id, **dargs)
867
868
mbligh67647152008-11-19 00:18:14 +0000869 def get_acls(self):
870 return self.afe.get_acls(hosts__hostname=self.hostname)
871
872
873 def add_acl(self, acl_name):
874 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
875 return self.afe.run('acl_group_add_hosts', id=acl_name,
876 hosts=[self.hostname])
877
878
879 def remove_acl(self, acl_name):
880 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
881 return self.afe.run('acl_group_remove_hosts', id=acl_name,
882 hosts=[self.hostname])
883
884
885 def get_labels(self):
886 return self.afe.get_labels(host__hostname__in=[self.hostname])
887
888
889 def add_labels(self, labels):
890 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
891 return self.afe.run('host_add_labels', id=self.id, labels=labels)
892
893
894 def remove_labels(self, labels):
895 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
896 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000897
898
mbligh54459c72009-01-21 19:26:44 +0000899class User(RpcObject):
900 def __repr__(self):
901 return 'USER: %s' % self.login
902
903
mbligh5280e3b2008-12-22 14:39:28 +0000904class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000905 """
906 TKO test status object
907
908 Fields:
909 test_idx, hostname, testname, id
910 complete_count, incomplete_count, group_count, pass_count
911 """
912 def __repr__(self):
913 return 'TEST STATUS: %s' % self.id
914
915
MK Ryuacf35922014-10-03 14:56:49 -0700916class HostAttribute(RpcObject):
917 """
918 AFE host attribute object
919
920 Fields:
921 id, host, attribute, value
922 """
923 def __repr__(self):
924 return 'HOST ATTRIBUTE %d' % self.id