blob: 453ee4c9bf8d8fbe84b2d4b434ef0b0294307e42 [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
Richard Barnette383ef9c2016-12-13 11:56:49 -0800315 def get_image_name(self, board):
316 """
317 Return the full image name of the stable version for `board`.
318
319 This finds the stable version for `board`, and returns a string
320 identifying the associated image file. The string represents
321 part of a URL for access to the image.
322
323 The image name is typically of a form like
324 "falco-release/R55-8872.44.0".
325
326 @return A string identifying the image file for the stable
327 image for `board`.
328 """
329 version = self.get_version(board)
Richard Barnette260cbd02016-10-06 12:23:28 -0700330 build_pattern = GLOBAL_CONFIG.get_config_value(
331 'CROS', 'stable_build_pattern')
332 return build_pattern % (board, version)
333
334
335class _AndroidVersionMap(_OSVersionMap):
336 """
337 Stable version mapping for Android release images.
338
339 This class manages a mapping of Android/Brillo board names to
340 known-good images.
341 """
342
343 def __init__(self, afe):
344 super(_AndroidVersionMap, self).__init__(afe, True)
345
346
347 def get_all_versions(self):
348 versions = super(_AndroidVersionMap, self).get_all_versions()
349 del versions[self.DEFAULT_BOARD]
350 return versions
351
352
Richard Barnettee50453e2016-10-10 16:43:44 -0700353class _SuffixHackVersionMap(_StableVersionMap):
Richard Barnette260cbd02016-10-06 12:23:28 -0700354 """
Richard Barnettee50453e2016-10-10 16:43:44 -0700355 Abstract super class for mappings using a pseudo-board name.
Richard Barnette260cbd02016-10-06 12:23:28 -0700356
Richard Barnettee50453e2016-10-10 16:43:44 -0700357 For non-OS image type mappings, we look them up in the
358 `stable_versions` table by constructing a "pseudo-board" from the
359 real board name plus a suffix string that identifies the image type.
360 So, for instance the name "lulu/firmware" is used to look up the
361 FAFT firmware version for lulu boards.
Richard Barnette260cbd02016-10-06 12:23:28 -0700362 """
363
Richard Barnettee50453e2016-10-10 16:43:44 -0700364 # _SUFFIX - The suffix used in constructing the "pseudo-board"
365 # lookup key. Each subclass must define this value for itself.
Richard Barnette260cbd02016-10-06 12:23:28 -0700366 #
Richard Barnettee50453e2016-10-10 16:43:44 -0700367 _SUFFIX = None
Richard Barnette260cbd02016-10-06 12:23:28 -0700368
369 def __init__(self, afe):
Richard Barnettee50453e2016-10-10 16:43:44 -0700370 super(_SuffixHackVersionMap, self).__init__(afe, False)
Richard Barnette260cbd02016-10-06 12:23:28 -0700371
372
373 def get_all_versions(self):
Richard Barnettee50453e2016-10-10 16:43:44 -0700374 # Get all the mappings from the AFE, extract just the mappings
375 # with our suffix, and replace the pseudo-board name keys with
Richard Barnette260cbd02016-10-06 12:23:28 -0700376 # the real board names.
377 #
378 all_versions = super(
Richard Barnettee50453e2016-10-10 16:43:44 -0700379 _SuffixHackVersionMap, self).get_all_versions()
Richard Barnette260cbd02016-10-06 12:23:28 -0700380 return {
381 board[0 : -len(self._SUFFIX)]: all_versions[board]
382 for board in all_versions.keys()
383 if board.endswith(self._SUFFIX)
384 }
385
386
387 def get_version(self, board):
Richard Barnettee50453e2016-10-10 16:43:44 -0700388 board += self._SUFFIX
389 return super(_SuffixHackVersionMap, self).get_version(board)
390
391
392 def set_version(self, board, version):
393 board += self._SUFFIX
394 super(_SuffixHackVersionMap, self).set_version(board, version)
395
396
397 def delete_version(self, board):
398 board += self._SUFFIX
399 super(_SuffixHackVersionMap, self).delete_version(board)
400
401
402class _FAFTVersionMap(_SuffixHackVersionMap):
403 """
404 Stable version mapping for firmware versions used in FAFT repair.
405
406 When DUTs used for FAFT fail repair, stable firmware may need to be
407 flashed directly from original tarballs. The FAFT firmware version
408 mapping finds the appropriate tarball for a given board.
409 """
410
411 _SUFFIX = '/firmware'
412
413 def get_version(self, board):
414 # If there's no mapping for `board`, the lookup will return the
415 # default CrOS version mapping. To eliminate that case, we
416 # require a '/' character in the version, since CrOS versions
417 # won't match that.
Richard Barnette260cbd02016-10-06 12:23:28 -0700418 #
419 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
420 # the right fix is to move handling to the RPC server side.
421 #
Richard Barnette260cbd02016-10-06 12:23:28 -0700422 version = super(_FAFTVersionMap, self).get_version(board)
423 return version if '/' in version else None
424
425
Richard Barnettee50453e2016-10-10 16:43:44 -0700426class _FirmwareVersionMap(_SuffixHackVersionMap):
427 """
428 Stable version mapping for firmware supplied in Chrome OS images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700429
Richard Barnettee50453e2016-10-10 16:43:44 -0700430 A Chrome OS image bundles a version of the firmware that the
431 device should update to when the OS version is installed during
432 AU.
Richard Barnette260cbd02016-10-06 12:23:28 -0700433
Richard Barnettee50453e2016-10-10 16:43:44 -0700434 Test images suppress the firmware update during AU. Instead, during
435 repair and verify we check installed firmware on a DUT, compare it
436 against the stable version mapping for the board, and update when
437 the DUT is out-of-date.
438 """
439
440 _SUFFIX = '/rwfw'
441
442 def get_version(self, board):
443 # If there's no mapping for `board`, the lookup will return the
444 # default CrOS version mapping. To eliminate that case, we
445 # require the version start with "Google_", since CrOS versions
446 # won't match that.
447 #
448 # TODO(jrbarnette): This is, of course, a hack. Ultimately,
449 # the right fix is to move handling to the RPC server side.
450 #
451 version = super(_FirmwareVersionMap, self).get_version(board)
452 return version if version.startswith('Google_') else None
Richard Barnette260cbd02016-10-06 12:23:28 -0700453
454
mbligh5280e3b2008-12-22 14:39:28 +0000455class AFE(RpcClient):
mbligh17c75e62009-06-08 16:18:21 +0000456 def __init__(self, user=None, server=None, print_log=True, debug=False,
mbligh99b24f42009-06-08 16:45:55 +0000457 reply_debug=False, job=None):
mbligh17c75e62009-06-08 16:18:21 +0000458 self.job = job
Scott Zawalski347aaf42012-04-03 16:33:00 -0400459 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000460 user=user,
461 server=server,
462 print_log=print_log,
463 debug=debug,
464 reply_debug=reply_debug)
mblighc31e4022008-12-11 19:32:30 +0000465
mbligh1ef218d2009-08-03 16:57:56 +0000466
Richard Barnette260cbd02016-10-06 12:23:28 -0700467 # Known image types for stable version mapping objects.
468 # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
469 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
Richard Barnettee50453e2016-10-10 16:43:44 -0700470 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
Richard Barnette260cbd02016-10-06 12:23:28 -0700471 # ANDROID_IMAGE_TYPE - Mappings for Android images.
472 #
473 CROS_IMAGE_TYPE = 'cros'
474 FAFT_IMAGE_TYPE = 'faft'
Richard Barnettee50453e2016-10-10 16:43:44 -0700475 FIRMWARE_IMAGE_TYPE = 'firmware'
Richard Barnette260cbd02016-10-06 12:23:28 -0700476 ANDROID_IMAGE_TYPE = 'android'
477
478 _IMAGE_MAPPING_CLASSES = {
479 CROS_IMAGE_TYPE: _CrosVersionMap,
480 FAFT_IMAGE_TYPE: _FAFTVersionMap,
Richard Barnettee50453e2016-10-10 16:43:44 -0700481 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
Richard Barnette260cbd02016-10-06 12:23:28 -0700482 ANDROID_IMAGE_TYPE: _AndroidVersionMap
483 }
484
485
486 def get_stable_version_map(self, image_type):
487 """
488 Return a stable version mapping for the given image type.
489
490 @return An object mapping board names to version strings for
491 software of the given image type.
492 """
493 return self._IMAGE_MAPPING_CLASSES[image_type](self)
494
495
mbligh67647152008-11-19 00:18:14 +0000496 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000497 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000498 statuses = self.run('get_static_data')['host_statuses']
499 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000500 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000501 if live == False:
502 return dead_statuses
503 else:
504 return statuses
505
506
mbligh71094012009-12-19 05:35:21 +0000507 @staticmethod
508 def _dict_for_host_query(hostnames=(), status=None, label=None):
509 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000510 if hostnames:
511 query_args['hostname__in'] = hostnames
512 if status:
513 query_args['status'] = status
514 if label:
515 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000516 return query_args
517
518
519 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
520 query_args = dict(dargs)
521 query_args.update(self._dict_for_host_query(hostnames=hostnames,
522 status=status,
523 label=label))
524 hosts = self.run('get_hosts', **query_args)
525 return [Host(self, h) for h in hosts]
526
527
528 def get_hostnames(self, status=None, label=None, **dargs):
529 """Like get_hosts() but returns hostnames instead of Host objects."""
530 # This implementation can be replaced with a more efficient one
531 # that does not query for entire host objects in the future.
532 return [host_obj.hostname for host_obj in
533 self.get_hosts(status=status, label=label, **dargs)]
534
535
536 def reverify_hosts(self, hostnames=(), status=None, label=None):
537 query_args = dict(locked=False,
538 aclgroup__users__login=self.user)
539 query_args.update(self._dict_for_host_query(hostnames=hostnames,
540 status=status,
541 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000542 return self.run('reverify_hosts', **query_args)
543
544
mbligh67647152008-11-19 00:18:14 +0000545 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000546 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000547 return self.get_hosts(id=id)[0]
548
549
MK Ryuacf35922014-10-03 14:56:49 -0700550 def get_host_attribute(self, attr, **dargs):
551 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
552 return [HostAttribute(self, a) for a in host_attrs]
553
554
Chris Masone8abb6fc2012-01-31 09:27:36 -0800555 def set_host_attribute(self, attr, val, **dargs):
556 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
557
558
mbligh67647152008-11-19 00:18:14 +0000559 def get_labels(self, **dargs):
560 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000561 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000562
563
564 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000565 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000566 return self.get_labels(id=id)[0]
567
568
569 def get_acls(self, **dargs):
570 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000571 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000572
573
574 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000575 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000576 return self.get_acls(id=id)[0]
577
578
mbligh54459c72009-01-21 19:26:44 +0000579 def get_users(self, **dargs):
580 users = self.run('get_users', **dargs)
581 return [User(self, u) for u in users]
582
583
mbligh1354c9d2008-12-22 14:56:13 +0000584 def generate_control_file(self, tests, **dargs):
585 ret = self.run('generate_control_file', tests=tests, **dargs)
586 return ControlFile(self, ret)
587
588
mbligh67647152008-11-19 00:18:14 +0000589 def get_jobs(self, summary=False, **dargs):
590 if summary:
591 jobs_data = self.run('get_jobs_summary', **dargs)
592 else:
593 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000594 jobs = []
595 for j in jobs_data:
596 job = Job(self, j)
597 # Set up some extra information defaults
598 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
599 job.platform_results = {}
600 job.platform_reasons = {}
601 jobs.append(job)
602 return jobs
mbligh67647152008-11-19 00:18:14 +0000603
604
605 def get_host_queue_entries(self, **data):
606 entries = self.run('get_host_queue_entries', **data)
mblighf9e35862009-02-26 01:03:11 +0000607 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000608
609 # Sadly, get_host_queue_entries doesn't return platforms, we have
610 # to get those back from an explicit get_hosts queury, then patch
611 # the new host objects back into the host list.
612 hostnames = [s.host.hostname for s in job_statuses if s.host]
613 host_hash = {}
614 for host in self.get_hosts(hostname__in=hostnames):
615 host_hash[host.hostname] = host
616 for status in job_statuses:
617 if status.host:
Fang Deng97dafbc2015-04-23 23:06:18 -0700618 status.host = host_hash.get(status.host.hostname)
mblighf9e35862009-02-26 01:03:11 +0000619 # filter job statuses that have either host or meta_host
620 return [status for status in job_statuses if (status.host or
621 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000622
623
MK Ryu1b2d7f92015-02-24 17:45:02 -0800624 def get_special_tasks(self, **data):
625 tasks = self.run('get_special_tasks', **data)
626 return [SpecialTask(self, t) for t in tasks]
627
628
J. Richard Barnette9f10c9f2015-04-13 16:44:50 -0700629 def get_host_special_tasks(self, host_id, **data):
630 tasks = self.run('get_host_special_tasks',
631 host_id=host_id, **data)
632 return [SpecialTask(self, t) for t in tasks]
633
634
J. Richard Barnette8dbd6d32015-05-01 11:01:12 -0700635 def get_host_status_task(self, host_id, end_time):
636 task = self.run('get_host_status_task',
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700637 host_id=host_id, end_time=end_time)
J. Richard Barnettebc9a7952015-04-16 17:43:27 -0700638 return SpecialTask(self, task) if task else None
639
640
J. Richard Barnette8abbfd62015-06-23 12:46:54 -0700641 def get_host_diagnosis_interval(self, host_id, end_time, success):
642 return self.run('get_host_diagnosis_interval',
643 host_id=host_id, end_time=end_time,
644 success=success)
645
646
mbligh67647152008-11-19 00:18:14 +0000647 def create_job(self, control_file, name=' ', priority='Medium',
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700648 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT, **dargs):
mbligh67647152008-11-19 00:18:14 +0000649 id = self.run('create_job', name=name, priority=priority,
650 control_file=control_file, control_type=control_type, **dargs)
651 return self.get_jobs(id=id)[0]
652
653
Simran Basi01984f52015-10-12 15:36:45 -0700654 def abort_jobs(self, jobs):
655 """Abort a list of jobs.
656
657 Already completed jobs will not be affected.
658
659 @param jobs: List of job ids to abort.
660 """
661 for job in jobs:
662 self.run('abort_host_queue_entries', job_id=job)
663
664
Kevin Cheng19521982016-09-22 12:27:23 -0700665 def get_hosts_by_attribute(self, attribute, value):
666 """
667 Get the list of hosts that share the same host attribute value.
668
669 @param attribute: String of the host attribute to check.
670 @param value: String of the value that is shared between hosts.
671
672 @returns List of hostnames that all have the same host attribute and
673 value.
674 """
675 return self.run('get_hosts_by_attribute',
676 attribute=attribute, value=value)
677
678
679 def lock_host(self, host, lock_reason, fail_if_locked=False):
680 """
681 Lock the given host with the given lock reason.
682
683 Locking a host that's already locked using the 'modify_hosts' rpc
684 will raise an exception. That's why fail_if_locked exists so the
685 caller can determine if the lock succeeded or failed. This will
686 save every caller from wrapping lock_host in a try-except.
687
688 @param host: hostname of host to lock.
689 @param lock_reason: Reason for locking host.
690 @param fail_if_locked: Return False if host is already locked.
691
692 @returns Boolean, True if lock was successful, False otherwise.
693 """
694 try:
695 self.run('modify_hosts',
696 host_filter_data={'hostname': host},
697 update_data={'locked': True,
698 'lock_reason': lock_reason})
699 except Exception:
700 return not fail_if_locked
701 return True
702
703
704 def unlock_hosts(self, locked_hosts):
705 """
706 Unlock the hosts.
707
708 Unlocking a host that's already unlocked will do nothing so we don't
709 need any special try-except clause here.
710
711 @param locked_hosts: List of hostnames of hosts to unlock.
712 """
713 self.run('modify_hosts',
714 host_filter_data={'hostname__in': locked_hosts},
715 update_data={'locked': False,
716 'lock_reason': ''})
717
718
mbligh5280e3b2008-12-22 14:39:28 +0000719class TestResults(object):
720 """
721 Container class used to hold the results of the tests for a job
722 """
723 def __init__(self):
724 self.good = []
725 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000726 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000727
728
729 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000730 if result.complete_count > result.pass_count:
731 self.fail.append(result)
732 elif result.incomplete_count > 0:
733 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000734 else:
mbligh451ede12009-02-12 21:54:03 +0000735 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000736
737
738class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000739 """
740 Generic object used to construct python objects from rpc calls
741 """
742 def __init__(self, afe, hash):
743 self.afe = afe
744 self.hash = hash
745 self.__dict__.update(hash)
746
747
748 def __str__(self):
749 return dump_object(self.__repr__(), self)
750
751
mbligh1354c9d2008-12-22 14:56:13 +0000752class ControlFile(RpcObject):
753 """
754 AFE control file object
755
756 Fields: synch_count, dependencies, control_file, is_server
757 """
758 def __repr__(self):
759 return 'CONTROL FILE: %s' % self.control_file
760
761
mbligh5280e3b2008-12-22 14:39:28 +0000762class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000763 """
764 AFE label object
765
766 Fields:
767 name, invalid, platform, kernel_config, id, only_if_needed
768 """
769 def __repr__(self):
770 return 'LABEL: %s' % self.name
771
772
773 def add_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800774 return self.afe.run('label_add_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000775
776
777 def remove_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800778 return self.afe.run('label_remove_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000779
780
mbligh5280e3b2008-12-22 14:39:28 +0000781class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000782 """
783 AFE acl object
784
785 Fields:
786 users, hosts, description, name, id
787 """
788 def __repr__(self):
789 return 'ACL: %s' % self.name
790
791
792 def add_hosts(self, hosts):
793 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
794 return self.afe.run('acl_group_add_hosts', self.id, hosts)
795
796
797 def remove_hosts(self, hosts):
798 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
799 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
800
801
mbligh54459c72009-01-21 19:26:44 +0000802 def add_users(self, users):
803 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
804 return self.afe.run('acl_group_add_users', id=self.name, users=users)
805
806
mbligh5280e3b2008-12-22 14:39:28 +0000807class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000808 """
809 AFE job object
810
811 Fields:
812 name, control_file, control_type, synch_count, reboot_before,
813 run_verify, priority, email_list, created_on, dependencies,
814 timeout, owner, reboot_after, id
815 """
816 def __repr__(self):
817 return 'JOB: %s' % self.id
818
819
mbligh5280e3b2008-12-22 14:39:28 +0000820class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000821 """
822 AFE job_status object
823
824 Fields:
825 status, complete, deleted, meta_host, host, active, execution_subdir, id
826 """
827 def __init__(self, afe, hash):
MK Ryu1b2d7f92015-02-24 17:45:02 -0800828 super(JobStatus, self).__init__(afe, hash)
mbligh5280e3b2008-12-22 14:39:28 +0000829 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700830 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000831 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000832
833
834 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000835 if self.host and self.host.hostname:
836 hostname = self.host.hostname
837 else:
838 hostname = 'None'
839 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000840
841
MK Ryu1b2d7f92015-02-24 17:45:02 -0800842class SpecialTask(RpcObject):
843 """
844 AFE special task object
845 """
846 def __init__(self, afe, hash):
847 super(SpecialTask, self).__init__(afe, hash)
848 self.host = Host(afe, self.host)
849
850
851 def __repr__(self):
852 return 'SPECIAL TASK: %s' % self.id
853
854
mbligh5280e3b2008-12-22 14:39:28 +0000855class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000856 """
857 AFE host object
858
859 Fields:
860 status, lock_time, locked_by, locked, hostname, invalid,
861 synch_id, labels, platform, protection, dirty, id
862 """
863 def __repr__(self):
864 return 'HOST OBJECT: %s' % self.hostname
865
866
867 def show(self):
868 labels = list(set(self.labels) - set([self.platform]))
869 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
870 self.locked, self.platform,
871 ', '.join(labels))
872
873
mbligh54459c72009-01-21 19:26:44 +0000874 def delete(self):
875 return self.afe.run('delete_host', id=self.id)
876
877
mbligh6463c4b2009-01-30 00:33:37 +0000878 def modify(self, **dargs):
879 return self.afe.run('modify_host', id=self.id, **dargs)
880
881
mbligh67647152008-11-19 00:18:14 +0000882 def get_acls(self):
883 return self.afe.get_acls(hosts__hostname=self.hostname)
884
885
886 def add_acl(self, acl_name):
887 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
888 return self.afe.run('acl_group_add_hosts', id=acl_name,
889 hosts=[self.hostname])
890
891
892 def remove_acl(self, acl_name):
893 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
894 return self.afe.run('acl_group_remove_hosts', id=acl_name,
895 hosts=[self.hostname])
896
897
898 def get_labels(self):
899 return self.afe.get_labels(host__hostname__in=[self.hostname])
900
901
902 def add_labels(self, labels):
903 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
904 return self.afe.run('host_add_labels', id=self.id, labels=labels)
905
906
907 def remove_labels(self, labels):
908 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
909 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000910
911
mbligh54459c72009-01-21 19:26:44 +0000912class User(RpcObject):
913 def __repr__(self):
914 return 'USER: %s' % self.login
915
916
mbligh5280e3b2008-12-22 14:39:28 +0000917class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000918 """
919 TKO test status object
920
921 Fields:
922 test_idx, hostname, testname, id
923 complete_count, incomplete_count, group_count, pass_count
924 """
925 def __repr__(self):
926 return 'TEST STATUS: %s' % self.id
927
928
MK Ryuacf35922014-10-03 14:56:49 -0700929class HostAttribute(RpcObject):
930 """
931 AFE host attribute object
932
933 Fields:
934 id, host, attribute, value
935 """
936 def __repr__(self):
937 return 'HOST ATTRIBUTE %d' % self.id