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