Home | History | Annotate | Download | only in hosts
      1 # Copyright 2016 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import abc
      6 import copy
      7 import logging
      8 
      9 import common
     10 from autotest_lib.server.cros import provision
     11 
     12 
     13 class HostInfo(object):
     14     """Holds label/attribute information about a host as understood by infra.
     15 
     16     This class is the source of truth of label / attribute information about a
     17     host for the test runner (autoserv) and the tests, *from the point of view
     18     of the infrastructure*.
     19 
     20     Typical usage:
     21         store = AfeHostInfoStore(...)
     22         host_info = store.get()
     23         update_somehow(host_info)
     24         store.commit(host_info)
     25 
     26     Besides the @property listed below, the following rw variables are part of
     27     the public API:
     28         labels: The list of labels for this host.
     29         attributes: The list of attributes for this host.
     30     """
     31 
     32     __slots__ = ['labels', 'attributes']
     33 
     34     # Constants related to exposing labels as more semantic properties.
     35     _BOARD_PREFIX = 'board'
     36     _OS_PREFIX = 'os'
     37     _POOL_PREFIX = 'pool'
     38 
     39     def __init__(self, labels=None, attributes=None):
     40         """
     41         @param labels: (optional list) labels to set on the HostInfo.
     42         @param attributes: (optional dict) attributes to set on the HostInfo.
     43         """
     44         self.labels = labels if labels is not None else []
     45         self.attributes = attributes if attributes is not None else {}
     46 
     47 
     48     @property
     49     def build(self):
     50         """Retrieve the current build for the host.
     51 
     52         TODO(pprabhu) Make provision.py depend on this instead of the other way
     53         around.
     54 
     55         @returns The first build label for this host (if there are multiple).
     56                 None if no build label is found.
     57         """
     58         for label_prefix in [provision.CROS_VERSION_PREFIX,
     59                             provision.ANDROID_BUILD_VERSION_PREFIX,
     60                             provision.TESTBED_BUILD_VERSION_PREFIX]:
     61             build_labels = self._get_stripped_labels_with_prefix(label_prefix)
     62             if build_labels:
     63                 return build_labels[0]
     64         return None
     65 
     66 
     67     @property
     68     def board(self):
     69         """Retrieve the board label value for the host.
     70 
     71         @returns: The (stripped) board label, or None if no label is found.
     72         """
     73         return self.get_label_value(self._BOARD_PREFIX)
     74 
     75 
     76     @property
     77     def os(self):
     78         """Retrieve the os for the host.
     79 
     80         @returns The os (str) or None if no os label exists. Returns the first
     81                 matching os if mutiple labels are found.
     82         """
     83         return self.get_label_value(self._OS_PREFIX)
     84 
     85 
     86     @property
     87     def pools(self):
     88         """Retrieve the set of pools for the host.
     89 
     90         @returns: set(str) of pool values.
     91         """
     92         return set(self._get_stripped_labels_with_prefix(self._POOL_PREFIX))
     93 
     94 
     95     def get_label_value(self, prefix):
     96         """Retrieve the value stored as a label with a well known prefix.
     97 
     98         @param prefix: The prefix of the desired label.
     99         @return: For the first label matching 'prefix:value', returns value.
    100                 Returns '' if no label matches the given prefix.
    101         """
    102         values = self._get_stripped_labels_with_prefix(prefix)
    103         return values[0] if values else ''
    104 
    105 
    106     def _get_stripped_labels_with_prefix(self, prefix):
    107         """Search for labels with the prefix and remove the prefix.
    108 
    109         e.g.
    110             prefix = blah
    111             labels = ['blah:a', 'blahb', 'blah:c', 'doo']
    112             returns: ['a', 'c']
    113 
    114         @returns: A list of stripped labels. [] in case of no match.
    115         """
    116         full_prefix = prefix + ':'
    117         prefix_len = len(full_prefix)
    118         return [label[prefix_len:] for label in self.labels
    119                 if label.startswith(full_prefix)]
    120 
    121 
    122     def __str__(self):
    123         return ('HostInfo [Labels: %s, Attributes: %s'
    124                 % (self.labels, self.attributes))
    125 
    126 
    127 class StoreError(Exception):
    128     """Raised when a CachingHostInfoStore operation fails."""
    129 
    130 
    131 class CachingHostInfoStore(object):
    132     """Abstract class to obtain and update host information from the infra.
    133 
    134     This class describes the API used to retrieve host information from the
    135     infrastructure. The actual, uncached implementation to obtain / update host
    136     information is delegated to the concrete store classes.
    137 
    138     We use two concrete stores:
    139         AfeHostInfoStore: Directly obtains/updates the host information from
    140                 the AFE.
    141         LocalHostInfoStore: Obtains/updates the host information from a local
    142                 file.
    143     An extra store is provided for unittests:
    144         InMemoryHostInfoStore: Just store labels / attributes in-memory.
    145     """
    146 
    147     __metaclass__ = abc.ABCMeta
    148 
    149     def __init__(self):
    150         self._private_cached_info = None
    151 
    152 
    153     def get(self, force_refresh=False):
    154         """Obtain (possibly cached) host information.
    155 
    156         @param force_refresh: If True, forces the cached HostInfo to be
    157                 refreshed from the store.
    158         @returns: A HostInfo object.
    159         """
    160         if force_refresh:
    161             return self._get_uncached()
    162 
    163         # |_cached_info| access is costly, so do it only once.
    164         info = self._cached_info
    165         if info is None:
    166             return self._get_uncached()
    167         return info
    168 
    169 
    170     def commit(self, info):
    171         """Update host information in the infrastructure.
    172 
    173         @param info: A HostInfo object with the new information to set. You
    174                 should obtain a HostInfo object using the |get| or
    175                 |get_uncached| methods, update it as needed and then commit.
    176         """
    177         logging.debug('Committing HostInfo to store %s', self)
    178         try:
    179             self._commit_impl(info)
    180             self._cached_info = info
    181             logging.debug('HostInfo updated to: %s', info)
    182         except Exception:
    183             self._cached_info = None
    184             raise
    185 
    186 
    187     @abc.abstractmethod
    188     def _refresh_impl(self):
    189         """Actual implementation to refresh host_info from the store.
    190 
    191         Concrete stores must implement this function.
    192         @returns: A HostInfo object.
    193         """
    194         raise NotImplementedError
    195 
    196 
    197     @abc.abstractmethod
    198     def _commit_impl(self, host_info):
    199         """Actual implementation to commit host_info to the store.
    200 
    201         Concrete stores must implement this function.
    202         @param host_info: A HostInfo object.
    203         """
    204         raise NotImplementedError
    205 
    206 
    207     def _get_uncached(self):
    208         """Obtain freshly synced host information.
    209 
    210         @returns: A HostInfo object.
    211         """
    212         logging.debug('Refreshing HostInfo using store %s', self)
    213         logging.debug('Old host_info: %s', self._cached_info)
    214         try:
    215             info = self._refresh_impl()
    216             self._cached_info = info
    217         except Exception:
    218             self._cached_info = None
    219             raise
    220 
    221         logging.debug('New host_info: %s', info)
    222         return info
    223 
    224 
    225     @property
    226     def _cached_info(self):
    227         """Access the cached info, enforcing a deepcopy."""
    228         return copy.deepcopy(self._private_cached_info)
    229 
    230 
    231     @_cached_info.setter
    232     def _cached_info(self, info):
    233         """Update the cached info, enforcing a deepcopy.
    234 
    235         @param info: The new info to update from.
    236         """
    237         self._private_cached_info = copy.deepcopy(info)
    238 
    239 
    240 class InMemoryHostInfoStore(CachingHostInfoStore):
    241     """A simple store that gives unittests direct access to backing data.
    242 
    243     Unittests can access the |info| attribute to obtain the backing HostInfo.
    244     """
    245 
    246     def __init__(self, info=None):
    247         """Seed object with initial data.
    248 
    249         @param info: Initial backing HostInfo object.
    250         """
    251         super(InMemoryHostInfoStore, self).__init__()
    252         self.info = info if info is not None else HostInfo()
    253 
    254 
    255     def _refresh_impl(self):
    256         """Return a copy of the private HostInfo."""
    257         return copy.deepcopy(self.info)
    258 
    259 
    260     def _commit_impl(self, info):
    261         """Copy HostInfo data to in-memory store.
    262 
    263         @param info: The HostInfo object to commit.
    264         """
    265         self.info = copy.deepcopy(info)
    266 
    267 
    268 def get_store_from_machine(machine):
    269     """Obtain the host_info_store object stuffed in the machine dict.
    270 
    271     The machine argument to jobs can be a string (a hostname) or a dict because
    272     of legacy reasons. If we can't get a real store, return a dummy.
    273     """
    274     if isinstance(machine, dict):
    275         return machine['host_info_store']
    276     else:
    277         return InMemoryHostInfoStore()
    278