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