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 """This class defines the Base Label classes."""
      6 
      7 
      8 import logging
      9 
     10 import common
     11 from autotest_lib.server.hosts import afe_store
     12 from autotest_lib.server.hosts import host_info
     13 from autotest_lib.server.hosts import shadowing_store
     14 
     15 
     16 def forever_exists_decorate(exists):
     17     """
     18     Decorator for labels that should exist forever once applied.
     19 
     20     We'll check if the label already exists on the host and return True if so.
     21     Otherwise we'll check if the label should exist on the host.
     22 
     23     @param exists: The exists method on the label class.
     24     """
     25     def exists_wrapper(self, host):
     26         """
     27         Wrapper around the label exists method.
     28 
     29         @param self: The label object.
     30         @param host: The host object to run methods on.
     31 
     32         @returns True if the label already exists on the host, otherwise run
     33             the exists method.
     34         """
     35         info = host.host_info_store.get()
     36         return (self._NAME in info.labels) or exists(self, host)
     37     return exists_wrapper
     38 
     39 
     40 class BaseLabel(object):
     41     """
     42     This class contains the scaffolding for the host-specific labels.
     43 
     44     @property _NAME String that is either the label returned or a prefix of a
     45                     generated label.
     46     """
     47 
     48     _NAME = None
     49 
     50     def generate_labels(self, host):
     51         """
     52         Return the list of labels generated for the host.
     53 
     54         @param host: The host object to check on.  Not needed here for base case
     55                      but could be needed for subclasses.
     56 
     57         @return a list of labels applicable to the host.
     58         """
     59         return [self._NAME]
     60 
     61 
     62     def exists(self, host):
     63         """
     64         Checks the host if the label is applicable or not.
     65 
     66         This method is geared for the type of labels that indicate if the host
     67         has a feature (bluetooth, touchscreen, etc) and as such require
     68         detection logic to determine if the label should be applicable to the
     69         host or not.
     70 
     71         @param host: The host object to check on.
     72         """
     73         raise NotImplementedError('exists not implemented')
     74 
     75 
     76     def get(self, host):
     77         """
     78         Return the list of labels.
     79 
     80         @param host: The host object to check on.
     81         """
     82         if self.exists(host):
     83             return self.generate_labels(host)
     84         else:
     85             return []
     86 
     87 
     88     def get_all_labels(self):
     89         """
     90         Return all possible labels generated by this label class.
     91 
     92         @returns a tuple of sets, the first set is for labels that are prefixes
     93             like 'os:android'.  The second set is for labels that are full
     94             labels by themselves like 'bluetooth'.
     95         """
     96         # Another subclass takes care of prefixed labels so this is empty.
     97         prefix_labels = set()
     98         full_labels_list = (self._NAME if isinstance(self._NAME, list) else
     99                             [self._NAME])
    100         full_labels = set(full_labels_list)
    101 
    102         return prefix_labels, full_labels
    103 
    104 
    105 class StringLabel(BaseLabel):
    106     """
    107     This class represents a string label that is dynamically generated.
    108 
    109     This label class is used for the types of label that are always
    110     present and will return at least one label out of a list of possible labels
    111     (listed in _NAME).  It is required that the subclasses implement
    112     generate_labels() since the label class will need to figure out which labels
    113     to return.
    114 
    115     _NAME must always be overridden by the subclass with all the possible
    116     labels that this label detection class can return in order to allow for
    117     accurate label updating.
    118     """
    119 
    120     def generate_labels(self, host):
    121         raise NotImplementedError('generate_labels not implemented')
    122 
    123 
    124     def exists(self, host):
    125         """Set to true since it is assumed the label is always applicable."""
    126         return True
    127 
    128 
    129 class StringPrefixLabel(StringLabel):
    130     """
    131     This class represents a string label that is dynamically generated.
    132 
    133     This label class is used for the types of label that usually are always
    134     present and indicate the os/board/etc type of the host.  The _NAME property
    135     will be prepended with a colon to the generated labels like so:
    136 
    137         _NAME = 'os'
    138         generate_label() returns ['android']
    139 
    140     The labels returned by this label class will be ['os:android'].
    141     It is important that the _NAME attribute be overridden by the
    142     subclass; otherwise, all labels returned will be prefixed with 'None:'.
    143     """
    144 
    145     def get(self, host):
    146         """Return the list of labels with _NAME prefixed with a colon.
    147 
    148         @param host: The host object to check on.
    149         """
    150         if self.exists(host):
    151             return ['%s:%s' % (self._NAME, label)
    152                     for label in self.generate_labels(host)]
    153         else:
    154             return []
    155 
    156 
    157     def get_all_labels(self):
    158         """
    159         Return all possible labels generated by this label class.
    160 
    161         @returns a tuple of sets, the first set is for labels that are prefixes
    162             like 'os:android'.  The second set is for labels that are full
    163             labels by themselves like 'bluetooth'.
    164         """
    165         # Since this is a prefix label class, we only care about
    166         # prefixed_labels.  We'll need to append the ':' to the label name to
    167         # make sure we only match on prefix labels.
    168         full_labels = set()
    169         prefix_labels = set(['%s:' % self._NAME])
    170 
    171         return prefix_labels, full_labels
    172 
    173 
    174 class LabelRetriever(object):
    175     """This class will assist in retrieving/updating the host labels."""
    176 
    177     def _populate_known_labels(self, label_list):
    178         """Create a list of known labels that is created through this class."""
    179         for label_instance in label_list:
    180             prefixed_labels, full_labels = label_instance.get_all_labels()
    181             self.label_prefix_names.update(prefixed_labels)
    182             self.label_full_names.update(full_labels)
    183 
    184 
    185     def __init__(self, label_list):
    186         self._labels = label_list
    187         # These two sets will contain the list of labels we can safely remove
    188         # during the update_labels call.
    189         self.label_full_names = set()
    190         self.label_prefix_names = set()
    191 
    192 
    193     def get_labels(self, host):
    194         """
    195         Retrieve the labels for the host.
    196 
    197         @param host: The host to get the labels for.
    198         """
    199         labels = []
    200         for label in self._labels:
    201             logging.info('checking label %s', label.__class__.__name__)
    202             try:
    203                 labels.extend(label.get(host))
    204             except Exception:
    205                 logging.exception('error getting label %s.',
    206                                   label.__class__.__name__)
    207         return labels
    208 
    209 
    210     def _is_known_label(self, label):
    211         """
    212         Checks if the label is a label known to the label detection framework.
    213 
    214         @param label: The label to check if we want to skip or not.
    215 
    216         @returns True to skip (which means to keep this label, False to remove.
    217         """
    218         return (label in self.label_full_names or
    219                 any([label.startswith(p) for p in self.label_prefix_names]))
    220 
    221 
    222     def _carry_over_unknown_labels(self, old_labels, new_labels):
    223         """Update new_labels by adding back old unknown labels.
    224 
    225         We only delete labels that we might have created earlier.  There are
    226         some labels we should not be removing (e.g. pool:bvt) that we
    227         want to keep but won't be part of the new labels detected on the host.
    228         To do that we compare the passed in label to our list of known labels
    229         and if we get a match, we feel safe knowing we can remove the label.
    230         Otherwise we leave that label alone since it was generated elsewhere.
    231 
    232         @param old_labels: List of labels already on the host.
    233         @param new_labels: List of newly detected labels. This list will be
    234                 updated to add back labels that are not tracked by the detection
    235                 framework.
    236         """
    237         missing_labels = set(old_labels) - set(new_labels)
    238         for label in missing_labels:
    239             if not self._is_known_label(label):
    240                 new_labels.append(label)
    241 
    242 
    243     def _commit_info(self, host, new_info, keep_pool):
    244         if keep_pool and isinstance(host.host_info_store,
    245                                     shadowing_store.ShadowingStore):
    246             primary_store = afe_store.AfeStoreKeepPool(host.hostname)
    247             host.host_info_store.commit_with_substitute(
    248                     new_info,
    249                     primary_store=primary_store,
    250                     shadow_store=None)
    251             return
    252 
    253         host.host_info_store.commit(new_info)
    254 
    255 
    256     def update_labels(self, host, keep_pool=False):
    257         """
    258         Retrieve the labels from the host and update if needed.
    259 
    260         @param host: The host to update the labels for.
    261         """
    262         # If we haven't yet grabbed our list of known labels, do so now.
    263         if not self.label_full_names and not self.label_prefix_names:
    264             self._populate_known_labels(self._labels)
    265 
    266         # Label detection hits the DUT so it can be slow. Do it before reading
    267         # old labels from HostInfoStore to minimize the time between read and
    268         # commit of the HostInfo.
    269         new_labels = self.get_labels(host)
    270         old_info = host.host_info_store.get()
    271         self._carry_over_unknown_labels(old_info.labels, new_labels)
    272         new_info = host_info.HostInfo(
    273                 labels=new_labels,
    274                 attributes=old_info.attributes,
    275         )
    276         if old_info != new_info:
    277             self._commit_info(host, new_info, keep_pool)
    278