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