| # 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. |
| |
| import abc |
| import copy |
| import logging |
| |
| import common |
| from autotest_lib.server.cros import provision |
| |
| |
| class HostInfo(object): |
| """Holds label/attribute information about a host as understood by infra. |
| |
| This class is the source of truth of label / attribute information about a |
| host for the test runner (autoserv) and the tests, *from the point of view |
| of the infrastructure*. |
| |
| Typical usage: |
| store = AfeHostInfoStore(...) |
| host_info = store.get() |
| update_somehow(host_info) |
| store.commit(host_info) |
| |
| Besides the @property listed below, the following rw variables are part of |
| the public API: |
| labels: The list of labels for this host. |
| attributes: The list of attributes for this host. |
| """ |
| |
| __slots__ = ['labels', 'attributes'] |
| |
| # Constants related to exposing labels as more semantic properties. |
| _BOARD_PREFIX = 'board' |
| _OS_PREFIX = 'os' |
| _POOL_PREFIX = 'pool' |
| |
| def __init__(self, labels=None, attributes=None): |
| """ |
| @param labels: (optional list) labels to set on the HostInfo. |
| @param attributes: (optional dict) attributes to set on the HostInfo. |
| """ |
| self.labels = labels if labels is not None else [] |
| self.attributes = attributes if attributes is not None else {} |
| |
| |
| @property |
| def build(self): |
| """Retrieve the current build for the host. |
| |
| TODO(pprabhu) Make provision.py depend on this instead of the other way |
| around. |
| |
| @returns The first build label for this host (if there are multiple). |
| None if no build label is found. |
| """ |
| for label_prefix in [provision.CROS_VERSION_PREFIX, |
| provision.ANDROID_BUILD_VERSION_PREFIX, |
| provision.TESTBED_BUILD_VERSION_PREFIX]: |
| build_labels = self._get_stripped_labels_with_prefix(label_prefix) |
| if build_labels: |
| return build_labels[0] |
| return None |
| |
| |
| @property |
| def board(self): |
| """Retrieve the board label value for the host. |
| |
| @returns: The (stripped) board label, or None if no label is found. |
| """ |
| return self.get_label_value(self._BOARD_PREFIX) |
| |
| |
| @property |
| def os(self): |
| """Retrieve the os for the host. |
| |
| @returns The os (str) or None if no os label exists. Returns the first |
| matching os if mutiple labels are found. |
| """ |
| return self.get_label_value(self._OS_PREFIX) |
| |
| |
| @property |
| def pools(self): |
| """Retrieve the set of pools for the host. |
| |
| @returns: set(str) of pool values. |
| """ |
| return set(self._get_stripped_labels_with_prefix(self._POOL_PREFIX)) |
| |
| |
| def get_label_value(self, prefix): |
| """Retrieve the value stored as a label with a well known prefix. |
| |
| @param prefix: The prefix of the desired label. |
| @return: For the first label matching 'prefix:value', returns value. |
| Returns '' if no label matches the given prefix. |
| """ |
| values = self._get_stripped_labels_with_prefix(prefix) |
| return values[0] if values else '' |
| |
| |
| def _get_stripped_labels_with_prefix(self, prefix): |
| """Search for labels with the prefix and remove the prefix. |
| |
| e.g. |
| prefix = blah |
| labels = ['blah:a', 'blahb', 'blah:c', 'doo'] |
| returns: ['a', 'c'] |
| |
| @returns: A list of stripped labels. [] in case of no match. |
| """ |
| full_prefix = prefix + ':' |
| prefix_len = len(full_prefix) |
| return [label[prefix_len:] for label in self.labels |
| if label.startswith(full_prefix)] |
| |
| |
| def __str__(self): |
| return ('HostInfo [Labels: %s, Attributes: %s' |
| % (self.labels, self.attributes)) |
| |
| |
| class StoreError(Exception): |
| """Raised when a CachingHostInfoStore operation fails.""" |
| |
| |
| class CachingHostInfoStore(object): |
| """Abstract class to obtain and update host information from the infra. |
| |
| This class describes the API used to retrieve host information from the |
| infrastructure. The actual, uncached implementation to obtain / update host |
| information is delegated to the concrete store classes. |
| |
| We use two concrete stores: |
| AfeHostInfoStore: Directly obtains/updates the host information from |
| the AFE. |
| LocalHostInfoStore: Obtains/updates the host information from a local |
| file. |
| An extra store is provided for unittests: |
| InMemoryHostInfoStore: Just store labels / attributes in-memory. |
| """ |
| |
| __metaclass__ = abc.ABCMeta |
| |
| def __init__(self): |
| self._private_cached_info = None |
| |
| |
| def get(self, force_refresh=False): |
| """Obtain (possibly cached) host information. |
| |
| @param force_refresh: If True, forces the cached HostInfo to be |
| refreshed from the store. |
| @returns: A HostInfo object. |
| """ |
| if force_refresh: |
| return self._get_uncached() |
| |
| # |_cached_info| access is costly, so do it only once. |
| info = self._cached_info |
| if info is None: |
| return self._get_uncached() |
| return info |
| |
| |
| def commit(self, info): |
| """Update host information in the infrastructure. |
| |
| @param info: A HostInfo object with the new information to set. You |
| should obtain a HostInfo object using the |get| or |
| |get_uncached| methods, update it as needed and then commit. |
| """ |
| logging.debug('Committing HostInfo to store %s', self) |
| try: |
| self._commit_impl(info) |
| self._cached_info = info |
| logging.debug('HostInfo updated to: %s', info) |
| except Exception: |
| self._cached_info = None |
| raise |
| |
| |
| @abc.abstractmethod |
| def _refresh_impl(self): |
| """Actual implementation to refresh host_info from the store. |
| |
| Concrete stores must implement this function. |
| @returns: A HostInfo object. |
| """ |
| raise NotImplementedError |
| |
| |
| @abc.abstractmethod |
| def _commit_impl(self, host_info): |
| """Actual implementation to commit host_info to the store. |
| |
| Concrete stores must implement this function. |
| @param host_info: A HostInfo object. |
| """ |
| raise NotImplementedError |
| |
| |
| def _get_uncached(self): |
| """Obtain freshly synced host information. |
| |
| @returns: A HostInfo object. |
| """ |
| logging.debug('Refreshing HostInfo using store %s', self) |
| logging.debug('Old host_info: %s', self._cached_info) |
| try: |
| info = self._refresh_impl() |
| self._cached_info = info |
| except Exception: |
| self._cached_info = None |
| raise |
| |
| logging.debug('New host_info: %s', info) |
| return info |
| |
| |
| @property |
| def _cached_info(self): |
| """Access the cached info, enforcing a deepcopy.""" |
| return copy.deepcopy(self._private_cached_info) |
| |
| |
| @_cached_info.setter |
| def _cached_info(self, info): |
| """Update the cached info, enforcing a deepcopy. |
| |
| @param info: The new info to update from. |
| """ |
| self._private_cached_info = copy.deepcopy(info) |
| |
| |
| class InMemoryHostInfoStore(CachingHostInfoStore): |
| """A simple store that gives unittests direct access to backing data. |
| |
| Unittests can access the |info| attribute to obtain the backing HostInfo. |
| """ |
| |
| def __init__(self, info=None): |
| """Seed object with initial data. |
| |
| @param info: Initial backing HostInfo object. |
| """ |
| super(InMemoryHostInfoStore, self).__init__() |
| self.info = info if info is not None else HostInfo() |
| |
| |
| def _refresh_impl(self): |
| """Return a copy of the private HostInfo.""" |
| return copy.deepcopy(self.info) |
| |
| |
| def _commit_impl(self, info): |
| """Copy HostInfo data to in-memory store. |
| |
| @param info: The HostInfo object to commit. |
| """ |
| self.info = copy.deepcopy(info) |
| |
| |
| def get_store_from_machine(machine): |
| """Obtain the host_info_store object stuffed in the machine dict. |
| |
| The machine argument to jobs can be a string (a hostname) or a dict because |
| of legacy reasons. If we can't get a real store, return a dummy. |
| """ |
| if isinstance(machine, dict): |
| return machine['host_info_store'] |
| else: |
| return InMemoryHostInfoStore() |