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 self._afe.run('set_stable_version', 245 version=version, board=board) 246 247 248 def delete_version(self, board): 249 """ 250 Remove the mapping of one board in the stable versions table. 251 252 Remove the mapping in the `stable_versions` table in the AFE 253 database for the given board. 254 255 @param board The board to be updated. 256 """ 257 self._afe.run('delete_stable_version', board=board) 258 259 260class _OSVersionMap(_StableVersionMap): 261 """ 262 Abstract stable version mapping for full OS images of various types. 263 """ 264 265 def _version_is_valid(self, version): 266 return True 267 268 def get_all_versions(self): 269 versions = super(_OSVersionMap, self).get_all_versions() 270 for board in versions.keys(): 271 if ('/' in board 272 or not self._version_is_valid(versions[board])): 273 del versions[board] 274 return versions 275 276 def get_version(self, board): 277 version = super(_OSVersionMap, self).get_version(board) 278 return version if self._version_is_valid(version) else None 279 280 281def format_cros_image_name(board, version): 282 """ 283 Return an image name for a given `board` and `version`. 284 285 This formats `board` and `version` into a string identifying an 286 image file. The string represents part of a URL for access to 287 the image. 288 289 The returned image name is typically of a form like 290 "falco-release/R55-8872.44.0". 291 """ 292 build_pattern = GLOBAL_CONFIG.get_config_value( 293 'CROS', 'stable_build_pattern') 294 return build_pattern % (board, version) 295 296 297class _CrosVersionMap(_OSVersionMap): 298 """ 299 Stable version mapping for Chrome OS release images. 300 301 This class manages a mapping of Chrome OS board names to known-good 302 release (or canary) images. The images selected can be installed on 303 DUTs during repair tasks, as a way of getting a DUT into a known 304 working state. 305 """ 306 307 def _version_is_valid(self, version): 308 return version is not None and '/' not in version 309 310 def get_image_name(self, board): 311 """ 312 Return the full image name of the stable version for `board`. 313 314 This finds the stable version for `board`, and returns a string 315 identifying the associated image as for `format_image_name()`, 316 above. 317 318 @return A string identifying the image file for the stable 319 image for `board`. 320 """ 321 return format_cros_image_name(board, self.get_version(board)) 322 323 324class _SuffixHackVersionMap(_StableVersionMap): 325 """ 326 Abstract super class for mappings using a pseudo-board name. 327 328 For non-OS image type mappings, we look them up in the 329 `stable_versions` table by constructing a "pseudo-board" from the 330 real board name plus a suffix string that identifies the image type. 331 So, for instance the name "lulu/firmware" is used to look up the 332 FAFT firmware version for lulu boards. 333 """ 334 335 # _SUFFIX - The suffix used in constructing the "pseudo-board" 336 # lookup key. Each subclass must define this value for itself. 337 # 338 _SUFFIX = None 339 340 def get_all_versions(self): 341 # Get all the mappings from the AFE, extract just the mappings 342 # with our suffix, and replace the pseudo-board name keys with 343 # the real board names. 344 # 345 all_versions = super( 346 _SuffixHackVersionMap, self).get_all_versions() 347 return { 348 board[0 : -len(self._SUFFIX)]: all_versions[board] 349 for board in all_versions.keys() 350 if board.endswith(self._SUFFIX) 351 } 352 353 354 def get_version(self, board): 355 board += self._SUFFIX 356 return super(_SuffixHackVersionMap, self).get_version(board) 357 358 359 def set_version(self, board, version): 360 board += self._SUFFIX 361 super(_SuffixHackVersionMap, self).set_version(board, version) 362 363 364 def delete_version(self, board): 365 board += self._SUFFIX 366 super(_SuffixHackVersionMap, self).delete_version(board) 367 368 369class _FAFTVersionMap(_SuffixHackVersionMap): 370 """ 371 Stable version mapping for firmware versions used in FAFT repair. 372 373 When DUTs used for FAFT fail repair, stable firmware may need to be 374 flashed directly from original tarballs. The FAFT firmware version 375 mapping finds the appropriate tarball for a given board. 376 """ 377 378 _SUFFIX = '/firmware' 379 380 def get_version(self, board): 381 # If there's no mapping for `board`, the lookup will return the 382 # default CrOS version mapping. To eliminate that case, we 383 # require a '/' character in the version, since CrOS versions 384 # won't match that. 385 # 386 # TODO(jrbarnette): This is, of course, a hack. Ultimately, 387 # the right fix is to move handling to the RPC server side. 388 # 389 version = super(_FAFTVersionMap, self).get_version(board) 390 return version if '/' in version else None 391 392 393class _FirmwareVersionMap(_SuffixHackVersionMap): 394 """ 395 Stable version mapping for firmware supplied in Chrome OS images. 396 397 A Chrome OS image bundles a version of the firmware that the 398 device should update to when the OS version is installed during 399 AU. 400 401 Test images suppress the firmware update during AU. Instead, during 402 repair and verify we check installed firmware on a DUT, compare it 403 against the stable version mapping for the board, and update when 404 the DUT is out-of-date. 405 """ 406 407 _SUFFIX = '/rwfw' 408 409 def get_version(self, board): 410 # If there's no mapping for `board`, the lookup will return the 411 # default CrOS version mapping. To eliminate that case, we 412 # require the version start with "Google_", since CrOS versions 413 # won't match that. 414 # 415 # TODO(jrbarnette): This is, of course, a hack. Ultimately, 416 # the right fix is to move handling to the RPC server side. 417 # 418 version = super(_FirmwareVersionMap, self).get_version(board) 419 return version if version.startswith('Google_') else None 420 421 422class AFE(RpcClient): 423 424 # Known image types for stable version mapping objects. 425 # CROS_IMAGE_TYPE - Mappings for Chrome OS images. 426 # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair. 427 # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images. 428 # 429 CROS_IMAGE_TYPE = 'cros' 430 FAFT_IMAGE_TYPE = 'faft' 431 FIRMWARE_IMAGE_TYPE = 'firmware' 432 433 _IMAGE_MAPPING_CLASSES = { 434 CROS_IMAGE_TYPE: _CrosVersionMap, 435 FAFT_IMAGE_TYPE: _FAFTVersionMap, 436 FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap, 437 } 438 439 440 def __init__(self, user=None, server=None, print_log=True, debug=False, 441 reply_debug=False, job=None): 442 self.job = job 443 super(AFE, self).__init__(path='/afe/server/noauth/rpc/', 444 user=user, 445 server=server, 446 print_log=print_log, 447 debug=debug, 448 reply_debug=reply_debug) 449 450 451 def get_stable_version_map(self, image_type): 452 """ 453 Return a stable version mapping for the given image type. 454 455 @return An object mapping board names to version strings for 456 software of the given image type. 457 """ 458 return self._IMAGE_MAPPING_CLASSES[image_type](self) 459 460 461 def host_statuses(self, live=None): 462 dead_statuses = ['Repair Failed', 'Repairing'] 463 statuses = self.run('get_static_data')['host_statuses'] 464 if live == True: 465 return list(set(statuses) - set(dead_statuses)) 466 if live == False: 467 return dead_statuses 468 else: 469 return statuses 470 471 472 @staticmethod 473 def _dict_for_host_query(hostnames=(), status=None, label=None): 474 query_args = {} 475 if hostnames: 476 query_args['hostname__in'] = hostnames 477 if status: 478 query_args['status'] = status 479 if label: 480 query_args['labels__name'] = label 481 return query_args 482 483 484 def get_hosts(self, hostnames=(), status=None, label=None, **dargs): 485 query_args = dict(dargs) 486 query_args.update(self._dict_for_host_query(hostnames=hostnames, 487 status=status, 488 label=label)) 489 hosts = self.run('get_hosts', **query_args) 490 return [Host(self, h) for h in hosts] 491 492 493 def get_hostnames(self, status=None, label=None, **dargs): 494 """Like get_hosts() but returns hostnames instead of Host objects.""" 495 # This implementation can be replaced with a more efficient one 496 # that does not query for entire host objects in the future. 497 return [host_obj.hostname for host_obj in 498 self.get_hosts(status=status, label=label, **dargs)] 499 500 501 def reverify_hosts(self, hostnames=(), status=None, label=None): 502 query_args = dict(locked=False, 503 aclgroup__users__login=self.user) 504 query_args.update(self._dict_for_host_query(hostnames=hostnames, 505 status=status, 506 label=label)) 507 return self.run('reverify_hosts', **query_args) 508 509 510 def repair_hosts(self, hostnames=(), status=None, label=None): 511 query_args = dict(locked=False, 512 aclgroup__users__login=self.user) 513 query_args.update(self._dict_for_host_query(hostnames=hostnames, 514 status=status, 515 label=label)) 516 return self.run('repair_hosts', **query_args) 517 518 519 def create_host(self, hostname, **dargs): 520 id = self.run('add_host', hostname=hostname, **dargs) 521 return self.get_hosts(id=id)[0] 522 523 524 def get_host_attribute(self, attr, **dargs): 525 host_attrs = self.run('get_host_attribute', attribute=attr, **dargs) 526 return [HostAttribute(self, a) for a in host_attrs] 527 528 529 def set_host_attribute(self, attr, val, **dargs): 530 self.run('set_host_attribute', attribute=attr, value=val, **dargs) 531 532 533 def get_labels(self, **dargs): 534 labels = self.run('get_labels', **dargs) 535 return [Label(self, l) for l in labels] 536 537 538 def create_label(self, name, **dargs): 539 id = self.run('add_label', name=name, **dargs) 540 return self.get_labels(id=id)[0] 541 542 543 def get_acls(self, **dargs): 544 acls = self.run('get_acl_groups', **dargs) 545 return [Acl(self, a) for a in acls] 546 547 548 def create_acl(self, name, **dargs): 549 id = self.run('add_acl_group', name=name, **dargs) 550 return self.get_acls(id=id)[0] 551 552 553 def get_users(self, **dargs): 554 users = self.run('get_users', **dargs) 555 return [User(self, u) for u in users] 556 557 558 def generate_control_file(self, tests, **dargs): 559 ret = self.run('generate_control_file', tests=tests, **dargs) 560 return ControlFile(self, ret) 561 562 563 def get_jobs(self, summary=False, **dargs): 564 if summary: 565 jobs_data = self.run('get_jobs_summary', **dargs) 566 else: 567 jobs_data = self.run('get_jobs', **dargs) 568 jobs = [] 569 for j in jobs_data: 570 job = Job(self, j) 571 # Set up some extra information defaults 572 job.testname = re.sub('\s.*', '', job.name) # arbitrary default 573 job.platform_results = {} 574 job.platform_reasons = {} 575 jobs.append(job) 576 return jobs 577 578 579 def get_host_queue_entries(self, **kwargs): 580 """Find JobStatus objects matching some constraints. 581 582 @param **kwargs: Arguments to pass to the RPC 583 """ 584 entries = self.run('get_host_queue_entries', **kwargs) 585 return self._entries_to_statuses(entries) 586 587 588 def get_host_queue_entries_by_insert_time(self, **kwargs): 589 """Like get_host_queue_entries, but using the insert index table. 590 591 @param **kwargs: Arguments to pass to the RPC 592 """ 593 entries = self.run('get_host_queue_entries_by_insert_time', **kwargs) 594 return self._entries_to_statuses(entries) 595 596 597 def _entries_to_statuses(self, entries): 598 """Converts HQEs to JobStatuses 599 600 Sadly, get_host_queue_entries doesn't return platforms, we have 601 to get those back from an explicit get_hosts queury, then patch 602 the new host objects back into the host list. 603 604 :param entries: A list of HQEs from get_host_queue_entries or 605 get_host_queue_entries_by_insert_time. 606 """ 607 job_statuses = [JobStatus(self, e) for e in entries] 608 hostnames = [s.host.hostname for s in job_statuses if s.host] 609 hosts = {} 610 for host in self.get_hosts(hostname__in=hostnames): 611 hosts[host.hostname] = host 612 for status in job_statuses: 613 if status.host: 614 status.host = hosts.get(status.host.hostname) 615 # filter job statuses that have either host or meta_host 616 return [status for status in job_statuses if (status.host or 617 status.meta_host)] 618 619 620 def get_special_tasks(self, **data): 621 tasks = self.run('get_special_tasks', **data) 622 return [SpecialTask(self, t) for t in tasks] 623 624 625 def get_host_special_tasks(self, host_id, **data): 626 tasks = self.run('get_host_special_tasks', 627 host_id=host_id, **data) 628 return [SpecialTask(self, t) for t in tasks] 629 630 631 def get_host_status_task(self, host_id, end_time): 632 task = self.run('get_host_status_task', 633 host_id=host_id, end_time=end_time) 634 return SpecialTask(self, task) if task else None 635 636 637 def get_host_diagnosis_interval(self, host_id, end_time, success): 638 return self.run('get_host_diagnosis_interval', 639 host_id=host_id, end_time=end_time, 640 success=success) 641 642 643 def create_job(self, control_file, name=' ', 644 priority=priorities.Priority.DEFAULT, 645 control_type=control_data.CONTROL_TYPE_NAMES.CLIENT, 646 **dargs): 647 id = self.run('create_job', name=name, priority=priority, 648 control_file=control_file, control_type=control_type, **dargs) 649 return self.get_jobs(id=id)[0] 650 651 652 def abort_jobs(self, jobs): 653 """Abort a list of jobs. 654 655 Already completed jobs will not be affected. 656 657 @param jobs: List of job ids to abort. 658 """ 659 for job in jobs: 660 self.run('abort_host_queue_entries', job_id=job) 661 662 663 def get_hosts_by_attribute(self, attribute, value): 664 """ 665 Get the list of hosts that share the same host attribute value. 666 667 @param attribute: String of the host attribute to check. 668 @param value: String of the value that is shared between hosts. 669 670 @returns List of hostnames that all have the same host attribute and 671 value. 672 """ 673 return self.run('get_hosts_by_attribute', 674 attribute=attribute, value=value) 675 676 677 def lock_host(self, host, lock_reason, fail_if_locked=False): 678 """ 679 Lock the given host with the given lock reason. 680 681 Locking a host that's already locked using the 'modify_hosts' rpc 682 will raise an exception. That's why fail_if_locked exists so the 683 caller can determine if the lock succeeded or failed. This will 684 save every caller from wrapping lock_host in a try-except. 685 686 @param host: hostname of host to lock. 687 @param lock_reason: Reason for locking host. 688 @param fail_if_locked: Return False if host is already locked. 689 690 @returns Boolean, True if lock was successful, False otherwise. 691 """ 692 try: 693 self.run('modify_hosts', 694 host_filter_data={'hostname': host}, 695 update_data={'locked': True, 696 'lock_reason': lock_reason}) 697 except Exception: 698 return not fail_if_locked 699 return True 700 701 702 def unlock_hosts(self, locked_hosts): 703 """ 704 Unlock the hosts. 705 706 Unlocking a host that's already unlocked will do nothing so we don't 707 need any special try-except clause here. 708 709 @param locked_hosts: List of hostnames of hosts to unlock. 710 """ 711 self.run('modify_hosts', 712 host_filter_data={'hostname__in': locked_hosts}, 713 update_data={'locked': False, 714 'lock_reason': ''}) 715 716 717class TestResults(object): 718 """ 719 Container class used to hold the results of the tests for a job 720 """ 721 def __init__(self): 722 self.good = [] 723 self.fail = [] 724 self.pending = [] 725 726 727 def add(self, result): 728 if result.complete_count > result.pass_count: 729 self.fail.append(result) 730 elif result.incomplete_count > 0: 731 self.pending.append(result) 732 else: 733 self.good.append(result) 734 735 736class RpcObject(object): 737 """ 738 Generic object used to construct python objects from rpc calls 739 """ 740 def __init__(self, afe, hash): 741 self.afe = afe 742 self.hash = hash 743 self.__dict__.update(hash) 744 745 746 def __str__(self): 747 return dump_object(self.__repr__(), self) 748 749 750class ControlFile(RpcObject): 751 """ 752 AFE control file object 753 754 Fields: synch_count, dependencies, control_file, is_server 755 """ 756 def __repr__(self): 757 return 'CONTROL FILE: %s' % self.control_file 758 759 760class Label(RpcObject): 761 """ 762 AFE label object 763 764 Fields: 765 name, invalid, platform, kernel_config, id, only_if_needed 766 """ 767 def __repr__(self): 768 return 'LABEL: %s' % self.name 769 770 771 def add_hosts(self, hosts): 772 # We must use the label's name instead of the id because label ids are 773 # not consistent across master-shard. 774 return self.afe.run('label_add_hosts', id=self.name, hosts=hosts) 775 776 777 def remove_hosts(self, hosts): 778 # We must use the label's name instead of the id because label ids are 779 # not consistent across master-shard. 780 return self.afe.run('label_remove_hosts', id=self.name, hosts=hosts) 781 782 783class Acl(RpcObject): 784 """ 785 AFE acl object 786 787 Fields: 788 users, hosts, description, name, id 789 """ 790 def __repr__(self): 791 return 'ACL: %s' % self.name 792 793 794 def add_hosts(self, hosts): 795 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name)) 796 return self.afe.run('acl_group_add_hosts', self.id, hosts) 797 798 799 def remove_hosts(self, hosts): 800 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name)) 801 return self.afe.run('acl_group_remove_hosts', self.id, hosts) 802 803 804 def add_users(self, users): 805 self.afe.log('Adding users %s to ACL %s' % (users, self.name)) 806 return self.afe.run('acl_group_add_users', id=self.name, users=users) 807 808 809class Job(RpcObject): 810 """ 811 AFE job object 812 813 Fields: 814 name, control_file, control_type, synch_count, reboot_before, 815 run_verify, priority, email_list, created_on, dependencies, 816 timeout, owner, reboot_after, id 817 """ 818 def __repr__(self): 819 return 'JOB: %s' % self.id 820 821 822class JobStatus(RpcObject): 823 """ 824 AFE job_status object 825 826 Fields: 827 status, complete, deleted, meta_host, host, active, execution_subdir, id 828 """ 829 def __init__(self, afe, hash): 830 super(JobStatus, self).__init__(afe, hash) 831 self.job = Job(afe, self.job) 832 if getattr(self, 'host'): 833 self.host = Host(afe, self.host) 834 835 836 def __repr__(self): 837 if self.host and self.host.hostname: 838 hostname = self.host.hostname 839 else: 840 hostname = 'None' 841 return 'JOB STATUS: %s-%s' % (self.job.id, hostname) 842 843 844class SpecialTask(RpcObject): 845 """ 846 AFE special task object 847 """ 848 def __init__(self, afe, hash): 849 super(SpecialTask, self).__init__(afe, hash) 850 self.host = Host(afe, self.host) 851 852 853 def __repr__(self): 854 return 'SPECIAL TASK: %s' % self.id 855 856 857class Host(RpcObject): 858 """ 859 AFE host object 860 861 Fields: 862 status, lock_time, locked_by, locked, hostname, invalid, 863 labels, platform, protection, dirty, id 864 """ 865 def __repr__(self): 866 return 'HOST OBJECT: %s' % self.hostname 867 868 869 def show(self): 870 labels = list(set(self.labels) - set([self.platform])) 871 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status, 872 self.locked, self.platform, 873 ', '.join(labels)) 874 875 876 def delete(self): 877 return self.afe.run('delete_host', id=self.id) 878 879 880 def modify(self, **dargs): 881 return self.afe.run('modify_host', id=self.id, **dargs) 882 883 884 def get_acls(self): 885 return self.afe.get_acls(hosts__hostname=self.hostname) 886 887 888 def add_acl(self, acl_name): 889 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname)) 890 return self.afe.run('acl_group_add_hosts', id=acl_name, 891 hosts=[self.hostname]) 892 893 894 def remove_acl(self, acl_name): 895 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname)) 896 return self.afe.run('acl_group_remove_hosts', id=acl_name, 897 hosts=[self.hostname]) 898 899 900 def get_labels(self): 901 return self.afe.get_labels(host__hostname__in=[self.hostname]) 902 903 904 def add_labels(self, labels): 905 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname)) 906 return self.afe.run('host_add_labels', id=self.id, labels=labels) 907 908 909 def remove_labels(self, labels): 910 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname)) 911 return self.afe.run('host_remove_labels', id=self.id, labels=labels) 912 913 914 def is_available(self): 915 """Check whether DUT host is available. 916 917 @return: bool 918 """ 919 return not (self.locked 920 or self.status in host_states.UNAVAILABLE_STATES) 921 922 923class User(RpcObject): 924 def __repr__(self): 925 return 'USER: %s' % self.login 926 927 928class TestStatus(RpcObject): 929 """ 930 TKO test status object 931 932 Fields: 933 test_idx, hostname, testname, id 934 complete_count, incomplete_count, group_count, pass_count 935 """ 936 def __repr__(self): 937 return 'TEST STATUS: %s' % self.id 938 939 940class HostAttribute(RpcObject): 941 """ 942 AFE host attribute object 943 944 Fields: 945 id, host, attribute, value 946 """ 947 def __repr__(self): 948 return 'HOST ATTRIBUTE %d' % self.id 949