blob: f51cac6381a38ab826232268e5526d40bc50c74c [file] [log] [blame]
# 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()