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