1 # Copyright Martin J. Bligh, Google Inc 2008 2 # Released under the GPL v2 3 4 """ 5 This class allows you to communicate with the frontend to submit jobs etc 6 It is designed for writing more sophisiticated server-side control files that 7 can recursively add and manage other jobs. 8 9 We turn the JSON dictionaries into real objects that are more idiomatic 10 11 For docs, see: 12 http://www.chromium.org/chromium-os/testing/afe-rpc-infrastructure 13 http://docs.djangoproject.com/en/dev/ref/models/querysets/#queryset-api 14 """ 15 16 #pylint: disable=missing-docstring 17 18 import getpass 19 import os 20 import re 21 22 import common 23 24 from autotest_lib.frontend.afe import rpc_client_lib 25 from autotest_lib.client.common_lib import control_data 26 from autotest_lib.client.common_lib import global_config 27 from autotest_lib.client.common_lib import priorities 28 from autotest_lib.client.common_lib import utils 29 from autotest_lib.tko import db 30 31 try: 32 from chromite.lib import metrics 33 except ImportError: 34 metrics = utils.metrics_mock 35 36 try: 37 from autotest_lib.server.site_common import site_utils as server_utils 38 except: 39 from autotest_lib.server import utils as server_utils 40 form_ntuples_from_machines = server_utils.form_ntuples_from_machines 41 42 GLOBAL_CONFIG = global_config.global_config 43 DEFAULT_SERVER = 'autotest' 44 45 46 def dump_object(header, obj): 47 """ 48 Standard way to print out the frontend objects (eg job, host, acl, label) 49 in a human-readable fashion for debugging 50 """ 51 result = header + '\n' 52 for key in obj.hash: 53 if key == 'afe' or key == 'hash': 54 continue 55 result += '%20s: %s\n' % (key, obj.hash[key]) 56 return result 57 58 59 class RpcClient(object): 60 """ 61 Abstract RPC class for communicating with the autotest frontend 62 Inherited for both TKO and AFE uses. 63 64 All the constructors go in the afe / tko class. 65 Manipulating methods go in the object classes themselves 66 """ 67 def __init__(self, path, user, server, print_log, debug, reply_debug): 68 """ 69 Create a cached instance of a connection to the frontend 70 71 user: username to connect as 72 server: frontend server to connect to 73 print_log: pring a logging message to stdout on every operation 74 debug: print out all RPC traffic 75 """ 76 if not user and utils.is_in_container(): 77 user = GLOBAL_CONFIG.get_config_value('SSP', 'user', default=None) 78 if not user: 79 user = getpass.getuser() 80 if not server: 81 if 'AUTOTEST_WEB' in os.environ: 82 server = os.environ['AUTOTEST_WEB'] 83 else: 84 server = GLOBAL_CONFIG.get_config_value('SERVER', 'hostname', 85 default=DEFAULT_SERVER) 86 self.server = server 87 self.user = user 88 self.print_log = print_log 89 self.debug = debug 90 self.reply_debug = reply_debug 91 headers = {'AUTHORIZATION': self.user} 92 rpc_server = 'http://' + server + path 93 if debug: 94 print 'SERVER: %s' % rpc_server 95 print 'HEADERS: %s' % headers 96 self.proxy = rpc_client_lib.get_proxy(rpc_server, headers=headers) 97 98 99 def run(self, call, **dargs): 100 """ 101 Make a RPC call to the AFE server 102 """ 103 rpc_call = getattr(self.proxy, call) 104 if self.debug: 105 print 'DEBUG: %s %s' % (call, dargs) 106 try: 107 result = utils.strip_unicode(rpc_call(**dargs)) 108 if self.reply_debug: 109 print result 110 return result 111 except Exception: 112 raise 113 114 115 def log(self, message): 116 if self.print_log: 117 print message 118 119 120 class TKO(RpcClient): 121 def __init__(self, user=None, server=None, print_log=True, debug=False, 122 reply_debug=False): 123 super(TKO, self).__init__(path='/new_tko/server/noauth/rpc/', 124 user=user, 125 server=server, 126 print_log=print_log, 127 debug=debug, 128 reply_debug=reply_debug) 129 self._db = None 130 131 132 @metrics.SecondsTimerDecorator( 133 'chromeos/autotest/tko/get_job_status_duration') 134 def get_job_test_statuses_from_db(self, job_id): 135 """Get job test statuses from the database. 136 137 Retrieve a set of fields from a job that reflect the status of each test 138 run within a job. 139 fields retrieved: status, test_name, reason, test_started_time, 140 test_finished_time, afe_job_id, job_owner, hostname. 141 142 @param job_id: The afe job id to look up. 143 @returns a TestStatus object of the resulting information. 144 """ 145 if self._db is None: 146 self._db = db.db() 147 fields = ['status', 'test_name', 'subdir', 'reason', 148 'test_started_time', 'test_finished_time', 'afe_job_id', 149 'job_owner', 'hostname', 'job_tag'] 150 table = 'tko_test_view_2' 151 where = 'job_tag like "%s-%%"' % job_id 152 test_status = [] 153 # Run commit before we query to ensure that we are pulling the latest 154 # results. 155 self._db.commit() 156 for entry in self._db.select(','.join(fields), table, (where, None)): 157 status_dict = {} 158 for key,value in zip(fields, entry): 159 # All callers expect values to be a str object. 160 status_dict[key] = str(value) 161 # id is used by TestStatus to uniquely identify each Test Status 162 # obj. 163 status_dict['id'] = [status_dict['reason'], status_dict['hostname'], 164 status_dict['test_name']] 165 test_status.append(status_dict) 166 167 return [TestStatus(self, e) for e in test_status] 168 169 170 def get_status_counts(self, job, **data): 171 entries = self.run('get_status_counts', 172 group_by=['hostname', 'test_name', 'reason'], 173 job_tag__startswith='%s-' % job, **data) 174 return [TestStatus(self, e) for e in entries['groups']] 175 176 177 class _StableVersionMap(object): 178 """ 179 A mapping from board names to strings naming software versions. 180 181 The mapping is meant to allow finding a nominally "stable" version 182 of software associated with a given board. The mapping identifies 183 specific versions of software that should be installed during 184 operations such as repair. 185 186 Conceptually, there are multiple version maps, each handling 187 different types of image. For instance, a single board may have 188 both a stable OS image (e.g. for CrOS), and a separate stable 189 firmware image. 190 191 Each different type of image requires a certain amount of special 192 handling, implemented by a subclass of `StableVersionMap`. The 193 subclasses take care of pre-processing of arguments, delegating 194 actual RPC calls to this superclass. 195 196 @property _afe AFE object through which to make the actual RPC 197 calls. 198 @property _android Value of the `android` parameter to be passed 199 when calling the `get_stable_version` RPC. 200 """ 201 202 # DEFAULT_BOARD - The stable_version RPC API recognizes this special 203 # name as a mapping to use when no specific mapping for a board is 204 # present. This default mapping is only allowed for CrOS image 205 # types; other image type subclasses exclude it. 206 # 207 # TODO(jrbarnette): This value is copied from 208 # site_utils.stable_version_utils, because if we import that 209 # module here, it breaks unit tests. Something about the Django 210 # setup... 211 DEFAULT_BOARD = 'DEFAULT' 212 213 214 def __init__(self, afe, android): 215 self._afe = afe 216 self._android = android 217 218 219 def get_all_versions(self): 220 """ 221 Get all mappings in the stable versions table. 222 223 Extracts the full content of the `stable_version` table 224 in the AFE database, and returns it as a dictionary 225 mapping board names to version strings. 226 227 @return A dictionary mapping board names to version strings. 228 """ 229 return self._afe.run('get_all_stable_versions') 230 231 232 def get_version(self, board): 233 """ 234 Get the mapping of one board in the stable versions table. 235 236 Look up and return the version mapped to the given board in the 237 `stable_versions` table in the AFE database. 238 239 @param board The board to be looked up. 240 241 @return The version mapped for the given board. 242 """ 243 return self._afe.run('get_stable_version', 244 board=board, android=self._android) 245 246 247 def set_version(self, board, version): 248 """ 249 Change the mapping of one board in the stable versions table. 250 251 Set the mapping in the `stable_versions` table in the AFE 252 database for the given board to the given version. 253 254 @param board The board to be updated. 255 @param version The new version to be assigned to the board. 256 """ 257 self._afe.run('set_stable_version', 258 version=version, board=board) 259 260 261 def delete_version(self, board): 262 """ 263 Remove the mapping of one board in the stable versions table. 264 265 Remove the mapping in the `stable_versions` table in the AFE 266 database for the given board. 267 268 @param board The board to be updated. 269 """ 270 self._afe.run('delete_stable_version', board=board) 271 272 273 class _OSVersionMap(_StableVersionMap): 274 """ 275 Abstract stable version mapping for full OS images of various types. 276 """ 277 278 def get_all_versions(self): 279 # TODO(jrbarnette): We exclude non-OS (i.e. firmware) version 280 # mappings, but the returned dict doesn't distinguish CrOS 281 # boards from Android boards; both will be present, and the 282 # subclass can't distinguish them. 283 # 284 # Ultimately, the right fix is to move knowledge of image type 285 # over to the RPC server side. 286 # 287 versions = super(_OSVersionMap, self).get_all_versions() 288 for board in versions.keys(): 289 if '/' in board: 290 del versions[board] 291 return versions 292 293 294 class _CrosVersionMap(_OSVersionMap): 295 """ 296 Stable version mapping for Chrome OS release images. 297 298 This class manages a mapping of Chrome OS board names to known-good 299 release (or canary) images. The images selected can be installed on 300 DUTs during repair tasks, as a way of getting a DUT into a known 301 working state. 302 """ 303 304 def __init__(self, afe): 305 super(_CrosVersionMap, self).__init__(afe, False) 306 307 @staticmethod 308 def format_image_name(board, version): 309 """ 310 Return an image name for a given `board` and `version`. 311 312 This formats `board` and `version` into a string identifying an 313 image file. The string represents part of a URL for access to 314 the image. 315 316 The returned image name is typically of a form like 317 "falco-release/R55-8872.44.0". 318 """ 319 build_pattern = GLOBAL_CONFIG.get_config_value( 320 'CROS', 'stable_build_pattern') 321 return build_pattern % (board, version) 322 323 def get_image_name(self, board): 324 """ 325 Return the full image name of the stable version for `board`. 326 327 This finds the stable version for `board`, and returns a string 328 identifying the associated image as for `format_image_name()`, 329 above. 330 331 @return A string identifying the image file for the stable 332 image for `board`. 333 """ 334 return self.format_image_name(board, self.get_version(board)) 335 336 337 class _AndroidVersionMap(_OSVersionMap): 338 """ 339 Stable version mapping for Android release images. 340 341 This class manages a mapping of Android/Brillo board names to 342 known-good images. 343 """ 344 345 def __init__(self, afe): 346 super(_AndroidVersionMap, self).__init__(afe, True) 347 348 349 def get_all_versions(self): 350 versions = super(_AndroidVersionMap, self).get_all_versions() 351 del versions[self.DEFAULT_BOARD] 352 return versions 353 354 355 class _SuffixHackVersionMap(_StableVersionMap): 356 """ 357 Abstract super class for mappings using a pseudo-board name. 358 359 For non-OS image type mappings, we look them up in the 360 `stable_versions` table by constructing a "pseudo-board" from the 361 real board name plus a suffix string that identifies the image type. 362 So, for instance the name "lulu/firmware" is used to look up the 363 FAFT firmware version for lulu boards. 364 """ 365 366 # _SUFFIX - The suffix used in constructing the "pseudo-board" 367 # lookup key. Each subclass must define this value for itself. 368 # 369 _SUFFIX = None 370 371 def __init__(self, afe): 372 super(_SuffixHackVersionMap, self).__init__(afe, False) 373 374 375 def get_all_versions(self): 376 # Get all the mappings from the AFE, extract just the mappings 377 # with our suffix, and replace the pseudo-board name keys with 378 # the real board names. 379 # 380 all_versions = super( 381 _SuffixHackVersionMap, self).get_all_versions() 382 return { 383 board[0 : -len(self._SUFFIX)]: all_versions[board] 384 for board in all_versions.keys() 385 if board.endswith(self._SUFFIX) 386 } 387 388 389 def get_version(self, board): 390 board += self._SUFFIX 391 return super(_SuffixHackVersionMap, self).get_version(board) 392 393 394 def set_version(self, board, version): 395 board += self._SUFFIX 396 super(_SuffixHackVersionMap, self).set_version(board, version) 397 398 399 def delete_version(self, board): 400 board += self._SUFFIX 401 super(_SuffixHackVersionMap, self).delete_version(board) 402 403 404 class _FAFTVersionMap(_SuffixHackVersionMap): 405 """ 406 Stable version mapping for firmware versions used in FAFT repair. 407 408 When DUTs used for FAFT fail repair, stable firmware may need to be 409 flashed directly from original tarballs. The FAFT firmware version 410 mapping finds the appropriate tarball for a given board. 411 """ 412 413 _SUFFIX = '/firmware' 414 415 def get_version(self, board): 416 # If there's no mapping for `board`, the lookup will return the 417 # default CrOS version mapping. To eliminate that case, we 418 # require a '/' character in the version, since CrOS versions 419 # won't match that. 420 # 421 # TODO(jrbarnette): This is, of course, a hack. Ultimately, 422 # the right fix is to move handling to the RPC server side. 423 # 424 version = super(_FAFTVersionMap, self).get_version(board) 425 return version if '/' in version else None 426 427 428 class _FirmwareVersionMap(_SuffixHackVersionMap): 429 """ 430 Stable version mapping for firmware supplied in Chrome OS images. 431 432 A Chrome OS image bundles a version of the firmware that the 433 device should update to when the OS version is installed during 434 AU. 435 436 Test images suppress the firmware update during AU. Instead, during 437 repair and verify we check installed firmware on a DUT, compare it 438 against the stable version mapping for the board, and update when 439 the DUT is out-of-date. 440 """ 441 442 _SUFFIX = '/rwfw' 443 444 def get_version(self, board): 445 # If there's no mapping for `board`, the lookup will return the 446 # default CrOS version mapping. To eliminate that case, we 447 # require the version start with "Google_", since CrOS versions 448 # won't match that. 449 # 450 # TODO(jrbarnette): This is, of course, a hack. Ultimately, 451 # the right fix is to move handling to the RPC server side. 452 # 453 version = super(_FirmwareVersionMap, self).get_version(board) 454 return version if version.startswith('Google_') else None 455 456 457 class AFE(RpcClient): 458 459 # Known image types for stable version mapping objects. 460 # CROS_IMAGE_TYPE - Mappings for Chrome OS images. 461 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair. 462 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images. 463 # ANDROID_IMAGE_TYPE - Mappings for Android images. 464 # 465 CROS_IMAGE_TYPE = 'cros' 466 FAFT_IMAGE_TYPE = 'faft' 467 FIRMWARE_IMAGE_TYPE = 'firmware' 468 ANDROID_IMAGE_TYPE = 'android' 469 470 _IMAGE_MAPPING_CLASSES = { 471 CROS_IMAGE_TYPE: _CrosVersionMap, 472 FAFT_IMAGE_TYPE: _FAFTVersionMap, 473 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap, 474 ANDROID_IMAGE_TYPE: _AndroidVersionMap 475 } 476 477 478 def __init__(self, user=None, server=None, print_log=True, debug=False, 479 reply_debug=False, job=None): 480 self.job = job 481 super(AFE, self).__init__(path='/afe/server/noauth/rpc/', 482 user=user, 483 server=server, 484 print_log=print_log, 485 debug=debug, 486 reply_debug=reply_debug) 487 488 489 def get_stable_version_map(self, image_type): 490 """ 491 Return a stable version mapping for the given image type. 492 493 @return An object mapping board names to version strings for 494 software of the given image type. 495 """ 496 return self._IMAGE_MAPPING_CLASSES[image_type](self) 497 498 499 def host_statuses(self, live=None): 500 dead_statuses = ['Repair Failed', 'Repairing'] 501 statuses = self.run('get_static_data')['host_statuses'] 502 if live == True: 503 return list(set(statuses) - set(dead_statuses)) 504 if live == False: 505 return dead_statuses 506 else: 507 return statuses 508 509 510 @staticmethod 511 def _dict_for_host_query(hostnames=(), status=None, label=None): 512 query_args = {} 513 if hostnames: 514 query_args['hostname__in'] = hostnames 515 if status: 516 query_args['status'] = status 517 if label: 518 query_args['labels__name'] = label 519 return query_args 520 521 522 def get_hosts(self, hostnames=(), status=None, label=None, **dargs): 523 query_args = dict(dargs) 524 query_args.update(self._dict_for_host_query(hostnames=hostnames, 525 status=status, 526 label=label)) 527 hosts = self.run('get_hosts', **query_args) 528 return [Host(self, h) for h in hosts] 529 530 531 def get_hostnames(self, status=None, label=None, **dargs): 532 """Like get_hosts() but returns hostnames instead of Host objects.""" 533 # This implementation can be replaced with a more efficient one 534 # that does not query for entire host objects in the future. 535 return [host_obj.hostname for host_obj in 536 self.get_hosts(status=status, label=label, **dargs)] 537 538 539 def reverify_hosts(self, hostnames=(), status=None, label=None): 540 query_args = dict(locked=False, 541 aclgroup__users__login=self.user) 542 query_args.update(self._dict_for_host_query(hostnames=hostnames, 543 status=status, 544 label=label)) 545 return self.run('reverify_hosts', **query_args) 546 547 548 def create_host(self, hostname, **dargs): 549 id = self.run('add_host', hostname=hostname, **dargs) 550 return self.get_hosts(id=id)[0] 551 552 553 def get_host_attribute(self, attr, **dargs): 554 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs) 555 return [HostAttribute(self, a) for a in host_attrs] 556 557 558 def set_host_attribute(self, attr, val, **dargs): 559 self.run('set_host_attribute', attribute=attr, value=val, **dargs) 560 561 562 def get_labels(self, **dargs): 563 labels = self.run('get_labels', **dargs) 564 return [Label(self, l) for l in labels] 565 566 567 def create_label(self, name, **dargs): 568 id = self.run('add_label', name=name, **dargs) 569 return self.get_labels(id=id)[0] 570 571 572 def get_acls(self, **dargs): 573 acls = self.run('get_acl_groups', **dargs) 574 return [Acl(self, a) for a in acls] 575 576 577 def create_acl(self, name, **dargs): 578 id = self.run('add_acl_group', name=name, **dargs) 579 return self.get_acls(id=id)[0] 580 581 582 def get_users(self, **dargs): 583 users = self.run('get_users', **dargs) 584 return [User(self, u) for u in users] 585 586 587 def generate_control_file(self, tests, **dargs): 588 ret = self.run('generate_control_file', tests=tests, **dargs) 589 return ControlFile(self, ret) 590 591 592 def get_jobs(self, summary=False, **dargs): 593 if summary: 594 jobs_data = self.run('get_jobs_summary', **dargs) 595 else: 596 jobs_data = self.run('get_jobs', **dargs) 597 jobs = [] 598 for j in jobs_data: 599 job = Job(self, j) 600 # Set up some extra information defaults 601 job.testname = re.sub('\s.*', '', job.name) # arbitrary default 602 job.platform_results = {} 603 job.platform_reasons = {} 604 jobs.append(job) 605 return jobs 606 607 608 def get_host_queue_entries(self, **kwargs): 609 """Find JobStatus objects matching some constraints. 610 611 @param **kwargs: Arguments to pass to the RPC 612 """ 613 entries = self.run('get_host_queue_entries', **kwargs) 614 return self._entries_to_statuses(entries) 615 616 617 def get_host_queue_entries_by_insert_time(self, **kwargs): 618 """Like get_host_queue_entries, but using the insert index table. 619 620 @param **kwargs: Arguments to pass to the RPC 621 """ 622 entries = self.run('get_host_queue_entries_by_insert_time', **kwargs) 623 return self._entries_to_statuses(entries) 624 625 626 def _entries_to_statuses(self, entries): 627 """Converts HQEs to JobStatuses 628 629 Sadly, get_host_queue_entries doesn't return platforms, we have 630 to get those back from an explicit get_hosts queury, then patch 631 the new host objects back into the host list. 632 633 :param entries: A list of HQEs from get_host_queue_entries or 634 get_host_queue_entries_by_insert_time. 635 """ 636 job_statuses = [JobStatus(self, e) for e in entries] 637 hostnames = [s.host.hostname for s in job_statuses if s.host] 638 hosts = {} 639 for host in self.get_hosts(hostname__in=hostnames): 640 hosts[host.hostname] = host 641 for status in job_statuses: 642 if status.host: 643 status.host = hosts.get(status.host.hostname) 644 # filter job statuses that have either host or meta_host 645 return [status for status in job_statuses if (status.host or 646 status.meta_host)] 647 648 649 def get_special_tasks(self, **data): 650 tasks = self.run('get_special_tasks', **data) 651 return [SpecialTask(self, t) for t in tasks] 652 653 654 def get_host_special_tasks(self, host_id, **data): 655 tasks = self.run('get_host_special_tasks', 656 host_id=host_id, **data) 657 return [SpecialTask(self, t) for t in tasks] 658 659 660 def get_host_status_task(self, host_id, end_time): 661 task = self.run('get_host_status_task', 662 host_id=host_id, end_time=end_time) 663 return SpecialTask(self, task) if task else None 664 665 666 def get_host_diagnosis_interval(self, host_id, end_time, success): 667 return self.run('get_host_diagnosis_interval', 668 host_id=host_id, end_time=end_time, 669 success=success) 670 671 672 def create_job(self, control_file, name=' ', 673 priority=priorities.Priority.DEFAULT, 674 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT, 675 **dargs): 676 id = self.run('create_job', name=name, priority=priority, 677 control_file=control_file, control_type=control_type, **dargs) 678 return self.get_jobs(id=id)[0] 679 680 681 def abort_jobs(self, jobs): 682 """Abort a list of jobs. 683 684 Already completed jobs will not be affected. 685 686 @param jobs: List of job ids to abort. 687 """ 688 for job in jobs: 689 self.run('abort_host_queue_entries', job_id=job) 690 691 692 def get_hosts_by_attribute(self, attribute, value): 693 """ 694 Get the list of hosts that share the same host attribute value. 695 696 @param attribute: String of the host attribute to check. 697 @param value: String of the value that is shared between hosts. 698 699 @returns List of hostnames that all have the same host attribute and 700 value. 701 """ 702 return self.run('get_hosts_by_attribute', 703 attribute=attribute, value=value) 704 705 706 def lock_host(self, host, lock_reason, fail_if_locked=False): 707 """ 708 Lock the given host with the given lock reason. 709 710 Locking a host that's already locked using the 'modify_hosts' rpc 711 will raise an exception. That's why fail_if_locked exists so the 712 caller can determine if the lock succeeded or failed. This will 713 save every caller from wrapping lock_host in a try-except. 714 715 @param host: hostname of host to lock. 716 @param lock_reason: Reason for locking host. 717 @param fail_if_locked: Return False if host is already locked. 718 719 @returns Boolean, True if lock was successful, False otherwise. 720 """ 721 try: 722 self.run('modify_hosts', 723 host_filter_data={'hostname': host}, 724 update_data={'locked': True, 725 'lock_reason': lock_reason}) 726 except Exception: 727 return not fail_if_locked 728 return True 729 730 731 def unlock_hosts(self, locked_hosts): 732 """ 733 Unlock the hosts. 734 735 Unlocking a host that's already unlocked will do nothing so we don't 736 need any special try-except clause here. 737 738 @param locked_hosts: List of hostnames of hosts to unlock. 739 """ 740 self.run('modify_hosts', 741 host_filter_data={'hostname__in': locked_hosts}, 742 update_data={'locked': False, 743 'lock_reason': ''}) 744 745 746 class TestResults(object): 747 """ 748 Container class used to hold the results of the tests for a job 749 """ 750 def __init__(self): 751 self.good = [] 752 self.fail = [] 753 self.pending = [] 754 755 756 def add(self, result): 757 if result.complete_count > result.pass_count: 758 self.fail.append(result) 759 elif result.incomplete_count > 0: 760 self.pending.append(result) 761 else: 762 self.good.append(result) 763 764 765 class RpcObject(object): 766 """ 767 Generic object used to construct python objects from rpc calls 768 """ 769 def __init__(self, afe, hash): 770 self.afe = afe 771 self.hash = hash 772 self.__dict__.update(hash) 773 774 775 def __str__(self): 776 return dump_object(self.__repr__(), self) 777 778 779 class ControlFile(RpcObject): 780 """ 781 AFE control file object 782 783 Fields: synch_count, dependencies, control_file, is_server 784 """ 785 def __repr__(self): 786 return 'CONTROL FILE: %s' % self.control_file 787 788 789 class Label(RpcObject): 790 """ 791 AFE label object 792 793 Fields: 794 name, invalid, platform, kernel_config, id, only_if_needed 795 """ 796 def __repr__(self): 797 return 'LABEL: %s' % self.name 798 799 800 def add_hosts(self, hosts): 801 return self.afe.run('label_add_hosts', id=self.id, hosts=hosts) 802 803 804 def remove_hosts(self, hosts): 805 return self.afe.run('label_remove_hosts', id=self.id, hosts=hosts) 806 807 808 class Acl(RpcObject): 809 """ 810 AFE acl object 811 812 Fields: 813 users, hosts, description, name, id 814 """ 815 def __repr__(self): 816 return 'ACL: %s' % self.name 817 818 819 def add_hosts(self, hosts): 820 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name)) 821 return self.afe.run('acl_group_add_hosts', self.id, hosts) 822 823 824 def remove_hosts(self, hosts): 825 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name)) 826 return self.afe.run('acl_group_remove_hosts', self.id, hosts) 827 828 829 def add_users(self, users): 830 self.afe.log('Adding users %s to ACL %s' % (users, self.name)) 831 return self.afe.run('acl_group_add_users', id=self.name, users=users) 832 833 834 class Job(RpcObject): 835 """ 836 AFE job object 837 838 Fields: 839 name, control_file, control_type, synch_count, reboot_before, 840 run_verify, priority, email_list, created_on, dependencies, 841 timeout, owner, reboot_after, id 842 """ 843 def __repr__(self): 844 return 'JOB: %s' % self.id 845 846 847 class JobStatus(RpcObject): 848 """ 849 AFE job_status object 850 851 Fields: 852 status, complete, deleted, meta_host, host, active, execution_subdir, id 853 """ 854 def __init__(self, afe, hash): 855 super(JobStatus, self).__init__(afe, hash) 856 self.job = Job(afe, self.job) 857 if getattr(self, 'host'): 858 self.host = Host(afe, self.host) 859 860 861 def __repr__(self): 862 if self.host and self.host.hostname: 863 hostname = self.host.hostname 864 else: 865 hostname = 'None' 866 return 'JOB STATUS: %s-%s' % (self.job.id, hostname) 867 868 869 class SpecialTask(RpcObject): 870 """ 871 AFE special task object 872 """ 873 def __init__(self, afe, hash): 874 super(SpecialTask, self).__init__(afe, hash) 875 self.host = Host(afe, self.host) 876 877 878 def __repr__(self): 879 return 'SPECIAL TASK: %s' % self.id 880 881 882 class Host(RpcObject): 883 """ 884 AFE host object 885 886 Fields: 887 status, lock_time, locked_by, locked, hostname, invalid, 888 labels, platform, protection, dirty, id 889 """ 890 def __repr__(self): 891 return 'HOST OBJECT: %s' % self.hostname 892 893 894 def show(self): 895 labels = list(set(self.labels) - set([self.platform])) 896 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status, 897 self.locked, self.platform, 898 ', '.join(labels)) 899 900 901 def delete(self): 902 return self.afe.run('delete_host', id=self.id) 903 904 905 def modify(self, **dargs): 906 return self.afe.run('modify_host', id=self.id, **dargs) 907 908 909 def get_acls(self): 910 return self.afe.get_acls(hosts__hostname=self.hostname) 911 912 913 def add_acl(self, acl_name): 914 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname)) 915 return self.afe.run('acl_group_add_hosts', id=acl_name, 916 hosts=[self.hostname]) 917 918 919 def remove_acl(self, acl_name): 920 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname)) 921 return self.afe.run('acl_group_remove_hosts', id=acl_name, 922 hosts=[self.hostname]) 923 924 925 def get_labels(self): 926 return self.afe.get_labels(host__hostname__in=[self.hostname]) 927 928 929 def add_labels(self, labels): 930 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname)) 931 return self.afe.run('host_add_labels', id=self.id, labels=labels) 932 933 934 def remove_labels(self, labels): 935 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname)) 936 return self.afe.run('host_remove_labels', id=self.id, labels=labels) 937 938 939 class User(RpcObject): 940 def __repr__(self): 941 return 'USER: %s' % self.login 942 943 944 class TestStatus(RpcObject): 945 """ 946 TKO test status object 947 948 Fields: 949 test_idx, hostname, testname, id 950 complete_count, incomplete_count, group_count, pass_count 951 """ 952 def __repr__(self): 953 return 'TEST STATUS: %s' % self.id 954 955 956 class HostAttribute(RpcObject): 957 """ 958 AFE host attribute object 959 960 Fields: 961 id, host, attribute, value 962 """ 963 def __repr__(self): 964 return 'HOST ATTRIBUTE %d' % self.id 965