# Copyright 2016 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """This class defines the Base Label classes.""" import logging import common from autotest_lib.server.hosts import afe_store from autotest_lib.server.hosts import host_info from autotest_lib.server.hosts import shadowing_store def forever_exists_decorate(exists): """ Decorator for labels that should exist forever once applied. We'll check if the label already exists on the host and return True if so. Otherwise we'll check if the label should exist on the host. @param exists: The exists method on the label class. """ def exists_wrapper(self, host): """ Wrapper around the label exists method. @param self: The label object. @param host: The host object to run methods on. @returns True if the label already exists on the host, otherwise run the exists method. """ info = host.host_info_store.get() return (self._NAME in info.labels) or exists(self, host) return exists_wrapper class BaseLabel(object): """ This class contains the scaffolding for the host-specific labels. @property _NAME String that is either the label returned or a prefix of a generated label. """ _NAME = None def generate_labels(self, host): """ Return the list of labels generated for the host. @param host: The host object to check on. Not needed here for base case but could be needed for subclasses. @return a list of labels applicable to the host. """ return [self._NAME] def exists(self, host): """ Checks the host if the label is applicable or not. This method is geared for the type of labels that indicate if the host has a feature (bluetooth, touchscreen, etc) and as such require detection logic to determine if the label should be applicable to the host or not. @param host: The host object to check on. """ raise NotImplementedError('exists not implemented') def get(self, host): """ Return the list of labels. @param host: The host object to check on. """ if self.exists(host): return self.generate_labels(host) else: return [] def get_all_labels(self): """ Return all possible labels generated by this label class. @returns a tuple of sets, the first set is for labels that are prefixes like 'os:android'. The second set is for labels that are full labels by themselves like 'bluetooth'. """ # Another subclass takes care of prefixed labels so this is empty. prefix_labels = set() full_labels_list = (self._NAME if isinstance(self._NAME, list) else [self._NAME]) full_labels = set(full_labels_list) return prefix_labels, full_labels def update_for_task(self, task_name): """ This method helps to check which labels need to be updated. State config labels are updated only for repair task. Lab config labels are updated only for deploy task. All labels are updated for any task. It is the responsibility of the subclass to override this method to differentiate itself as a state config label or a lab config label and return the appropriate boolean value. If the subclass doesn't override this method then that label will always be updated for any type of task. @returns True if labels should be updated for the task with given name """ return True class StringLabel(BaseLabel): """ This class represents a string label that is dynamically generated. This label class is used for the types of label that are always present and will return at least one label out of a list of possible labels (listed in _NAME). It is required that the subclasses implement generate_labels() since the label class will need to figure out which labels to return. _NAME must always be overridden by the subclass with all the possible labels that this label detection class can return in order to allow for accurate label updating. """ def generate_labels(self, host): raise NotImplementedError('generate_labels not implemented') def exists(self, host): """Set to true since it is assumed the label is always applicable.""" return True class StringPrefixLabel(StringLabel): """ This class represents a string label that is dynamically generated. This label class is used for the types of label that usually are always present and indicate the os/board/etc type of the host. The _NAME property will be prepended with a colon to the generated labels like so: _NAME = 'os' generate_label() returns ['android'] The labels returned by this label class will be ['os:android']. It is important that the _NAME attribute be overridden by the subclass; otherwise, all labels returned will be prefixed with 'None:'. """ def get(self, host): """Return the list of labels with _NAME prefixed with a colon. @param host: The host object to check on. """ if self.exists(host): return ['%s:%s' % (self._NAME, label) for label in self.generate_labels(host)] else: return [] def get_all_labels(self): """ Return all possible labels generated by this label class. @returns a tuple of sets, the first set is for labels that are prefixes like 'os:android'. The second set is for labels that are full labels by themselves like 'bluetooth'. """ # Since this is a prefix label class, we only care about # prefixed_labels. We'll need to append the ':' to the label name to # make sure we only match on prefix labels. full_labels = set() prefix_labels = set(['%s:' % self._NAME]) return prefix_labels, full_labels class LabelRetriever(object): """This class will assist in retrieving/updating the host labels.""" def _populate_known_labels(self, label_list, task_name): """Create a list of known labels that is created through this class.""" for label_instance in label_list: # populate only the labels that need to be updated for this task. if label_instance.update_for_task(task_name): prefixed_labels, full_labels = label_instance.get_all_labels() self.label_prefix_names.update(prefixed_labels) self.label_full_names.update(full_labels) def __init__(self, label_list): self._labels = label_list # These two sets will contain the list of labels we can safely remove # during the update_labels call. self.label_full_names = set() self.label_prefix_names = set() def get_labels(self, host): """ Retrieve the labels for the host. @param host: The host to get the labels for. """ labels = [] for label in self._labels: logging.info('checking label %s', label.__class__.__name__) try: labels.extend(label.get(host)) except Exception: logging.exception('error getting label %s.', label.__class__.__name__) return labels def get_labels_for_update(self, host, task_name): """ Retrieve the labels for the host which needs to be updated. @param host: The host to get the labels for updating. @param task_name: task name(repair/deploy) for the operation. @returns labels to be updated """ labels = [] for label in self._labels: try: # get only the labels which need to be updated for this task. if label.update_for_task(task_name): logging.info('checking label update %s', label.__class__.__name__) labels.extend(label.get(host)) except Exception: logging.exception('error getting label %s.', label.__class__.__name__) return labels def _is_known_label(self, label): """ Checks if the label is a label known to the label detection framework. @param label: The label to check if we want to skip or not. @returns True to skip (which means to keep this label, False to remove. """ return (label in self.label_full_names or any([label.startswith(p) for p in self.label_prefix_names])) def _carry_over_unknown_labels(self, old_labels, new_labels): """Update new_labels by adding back old unknown labels. We only delete labels that we might have created earlier. There are some labels we should not be removing (e.g. pool:bvt) that we want to keep but won't be part of the new labels detected on the host. To do that we compare the passed in label to our list of known labels and if we get a match, we feel safe knowing we can remove the label. Otherwise we leave that label alone since it was generated elsewhere. @param old_labels: List of labels already on the host. @param new_labels: List of newly detected labels. This list will be updated to add back labels that are not tracked by the detection framework. """ missing_labels = set(old_labels) - set(new_labels) for label in missing_labels: if not self._is_known_label(label): new_labels.append(label) def _commit_info(self, host, new_info, keep_pool): if keep_pool and isinstance(host.host_info_store, shadowing_store.ShadowingStore): primary_store = afe_store.AfeStoreKeepPool(host.hostname) host.host_info_store.commit_with_substitute( new_info, primary_store=primary_store, shadow_store=None) return host.host_info_store.commit(new_info) def update_labels(self, host, task_name='', keep_pool=False): """ Retrieve the labels from the host and update if needed. @param host: The host to update the labels for. """ # If we haven't yet grabbed our list of known labels, do so now. if not self.label_full_names and not self.label_prefix_names: self._populate_known_labels(self._labels, task_name) # Label detection hits the DUT so it can be slow. Do it before reading # old labels from HostInfoStore to minimize the time between read and # commit of the HostInfo. new_labels = self.get_labels_for_update(host, task_name) old_info = host.host_info_store.get() self._carry_over_unknown_labels(old_info.labels, new_labels) new_info = host_info.HostInfo( labels=new_labels, attributes=old_info.attributes, stable_versions=old_info.stable_versions, ) if old_info != new_info: self._commit_info(host, new_info, keep_pool)