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