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