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