• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5import abc
6import copy
7import json
8import logging
9
10import common
11from autotest_lib.server.cros import provision
12
13
14class HostInfo(object):
15    """Holds label/attribute information about a host as understood by infra.
16
17    This class is the source of truth of label / attribute information about a
18    host for the test runner (autoserv) and the tests, *from the point of view
19    of the infrastructure*.
20
21    Typical usage:
22        store = AfeHostInfoStore(...)
23        host_info = store.get()
24        update_somehow(host_info)
25        store.commit(host_info)
26
27    Besides the @property listed below, the following rw variables are part of
28    the public API:
29        labels: The list of labels for this host.
30        attributes: The list of attributes for this host.
31    """
32
33    __slots__ = ['labels', 'attributes']
34
35    # Constants related to exposing labels as more semantic properties.
36    _BOARD_PREFIX = 'board'
37    _OS_PREFIX = 'os'
38    _POOL_PREFIX = 'pool'
39
40    _VERSION_LABELS = (
41            provision.CROS_VERSION_PREFIX,
42            provision.CROS_TH_VERSION_PREFIX,
43            provision.ANDROID_BUILD_VERSION_PREFIX,
44            provision.TESTBED_BUILD_VERSION_PREFIX,
45    )
46
47    def __init__(self, labels=None, attributes=None):
48        """
49        @param labels: (optional list) labels to set on the HostInfo.
50        @param attributes: (optional dict) attributes to set on the HostInfo.
51        """
52        self.labels = labels if labels is not None else []
53        self.attributes = attributes if attributes is not None else {}
54
55
56    @property
57    def build(self):
58        """Retrieve the current build for the host.
59
60        TODO(pprabhu) Make provision.py depend on this instead of the other way
61        around.
62
63        @returns The first build label for this host (if there are multiple).
64                None if no build label is found.
65        """
66        for label_prefix in self._VERSION_LABELS:
67            build_labels = self._get_stripped_labels_with_prefix(label_prefix)
68            if build_labels:
69                return build_labels[0]
70        return None
71
72
73    @property
74    def board(self):
75        """Retrieve the board label value for the host.
76
77        @returns: The (stripped) board label, or None if no label is found.
78        """
79        return self.get_label_value(self._BOARD_PREFIX)
80
81
82    @property
83    def os(self):
84        """Retrieve the os for the host.
85
86        @returns The os (str) or None if no os label exists. Returns the first
87                matching os if mutiple labels are found.
88        """
89        return self.get_label_value(self._OS_PREFIX)
90
91
92    @property
93    def pools(self):
94        """Retrieve the set of pools for the host.
95
96        @returns: set(str) of pool values.
97        """
98        return set(self._get_stripped_labels_with_prefix(self._POOL_PREFIX))
99
100
101    def get_label_value(self, prefix):
102        """Retrieve the value stored as a label with a well known prefix.
103
104        @param prefix: The prefix of the desired label.
105        @return: For the first label matching 'prefix:value', returns value.
106                Returns '' if no label matches the given prefix.
107        """
108        values = self._get_stripped_labels_with_prefix(prefix)
109        return values[0] if values else ''
110
111
112    def clear_version_labels(self):
113        """Clear all version labels for the host."""
114        self.labels = [
115                label for label in self.labels if
116                not any(label.startswith(prefix + ':')
117                        for prefix in self._VERSION_LABELS)]
118
119
120    def set_version_label(self, version_prefix, version):
121        """Sets the version label for the host.
122
123        If a label with version_prefix exists, this updates the value for that
124        label, else appends a new label to the end of the label list.
125
126        @param version_prefix: The prefix to use (without the infix ':').
127        @param version: The version label value to set.
128        """
129        full_prefix = _to_label_prefix(version_prefix)
130        new_version_label = full_prefix + version
131        for index, label in enumerate(self.labels):
132            if label.startswith(full_prefix):
133                self.labels[index] = new_version_label
134                return
135        else:
136            self.labels.append(new_version_label)
137
138
139    def _get_stripped_labels_with_prefix(self, prefix):
140        """Search for labels with the prefix and remove the prefix.
141
142        e.g.
143            prefix = blah
144            labels = ['blah:a', 'blahb', 'blah:c', 'doo']
145            returns: ['a', 'c']
146
147        @returns: A list of stripped labels. [] in case of no match.
148        """
149        full_prefix = prefix + ':'
150        prefix_len = len(full_prefix)
151        return [label[prefix_len:] for label in self.labels
152                if label.startswith(full_prefix)]
153
154
155    def __str__(self):
156        return ('%s[Labels: %s, Attributes: %s]'
157                % (type(self).__name__, self.labels, self.attributes))
158
159
160    def __eq__(self, other):
161        if isinstance(other, type(self)):
162            return (self.labels == other.labels
163                    and self.attributes == other.attributes)
164        else:
165            return NotImplemented
166
167
168    def __ne__(self, other):
169        return not (self == other)
170
171
172class StoreError(Exception):
173    """Raised when a CachingHostInfoStore operation fails."""
174
175
176class CachingHostInfoStore(object):
177    """Abstract class to obtain and update host information from the infra.
178
179    This class describes the API used to retrieve host information from the
180    infrastructure. The actual, uncached implementation to obtain / update host
181    information is delegated to the concrete store classes.
182
183    We use two concrete stores:
184        AfeHostInfoStore: Directly obtains/updates the host information from
185                the AFE.
186        LocalHostInfoStore: Obtains/updates the host information from a local
187                file.
188    An extra store is provided for unittests:
189        InMemoryHostInfoStore: Just store labels / attributes in-memory.
190    """
191
192    __metaclass__ = abc.ABCMeta
193
194    def __init__(self):
195        self._private_cached_info = None
196
197
198    def get(self, force_refresh=False):
199        """Obtain (possibly cached) host information.
200
201        @param force_refresh: If True, forces the cached HostInfo to be
202                refreshed from the store.
203        @returns: A HostInfo object.
204        """
205        if force_refresh:
206            return self._get_uncached()
207
208        # |_cached_info| access is costly, so do it only once.
209        info = self._cached_info
210        if info is None:
211            return self._get_uncached()
212        return info
213
214
215    def commit(self, info):
216        """Update host information in the infrastructure.
217
218        @param info: A HostInfo object with the new information to set. You
219                should obtain a HostInfo object using the |get| or
220                |get_uncached| methods, update it as needed and then commit.
221        """
222        logging.debug('Committing HostInfo to store %s', self)
223        try:
224            self._commit_impl(info)
225            self._cached_info = info
226            logging.debug('HostInfo updated to: %s', info)
227        except Exception:
228            self._cached_info = None
229            raise
230
231
232    @abc.abstractmethod
233    def _refresh_impl(self):
234        """Actual implementation to refresh host_info from the store.
235
236        Concrete stores must implement this function.
237        @returns: A HostInfo object.
238        """
239        raise NotImplementedError
240
241
242    @abc.abstractmethod
243    def _commit_impl(self, host_info):
244        """Actual implementation to commit host_info to the store.
245
246        Concrete stores must implement this function.
247        @param host_info: A HostInfo object.
248        """
249        raise NotImplementedError
250
251
252    def _get_uncached(self):
253        """Obtain freshly synced host information.
254
255        @returns: A HostInfo object.
256        """
257        logging.debug('Refreshing HostInfo using store %s', self)
258        logging.debug('Old host_info: %s', self._cached_info)
259        try:
260            info = self._refresh_impl()
261            self._cached_info = info
262        except Exception:
263            self._cached_info = None
264            raise
265
266        logging.debug('New host_info: %s', info)
267        return info
268
269
270    @property
271    def _cached_info(self):
272        """Access the cached info, enforcing a deepcopy."""
273        return copy.deepcopy(self._private_cached_info)
274
275
276    @_cached_info.setter
277    def _cached_info(self, info):
278        """Update the cached info, enforcing a deepcopy.
279
280        @param info: The new info to update from.
281        """
282        self._private_cached_info = copy.deepcopy(info)
283
284
285class InMemoryHostInfoStore(CachingHostInfoStore):
286    """A simple store that gives unittests direct access to backing data.
287
288    Unittests can access the |info| attribute to obtain the backing HostInfo.
289    """
290
291    def __init__(self, info=None):
292        """Seed object with initial data.
293
294        @param info: Initial backing HostInfo object.
295        """
296        super(InMemoryHostInfoStore, self).__init__()
297        self.info = info if info is not None else HostInfo()
298
299
300    def __str__(self):
301        return '%s[%s]' % (type(self).__name__, self.info)
302
303    def _refresh_impl(self):
304        """Return a copy of the private HostInfo."""
305        return copy.deepcopy(self.info)
306
307
308    def _commit_impl(self, info):
309        """Copy HostInfo data to in-memory store.
310
311        @param info: The HostInfo object to commit.
312        """
313        self.info = copy.deepcopy(info)
314
315
316def get_store_from_machine(machine):
317    """Obtain the host_info_store object stuffed in the machine dict.
318
319    The machine argument to jobs can be a string (a hostname) or a dict because
320    of legacy reasons. If we can't get a real store, return a dummy.
321    """
322    if isinstance(machine, dict):
323        return machine['host_info_store']
324    else:
325        return InMemoryHostInfoStore()
326
327
328class DeserializationError(Exception):
329    """Raised when deserialization fails due to malformed input."""
330
331
332# Default serialzation version. This should be uprevved whenever a change to
333# HostInfo is backwards incompatible, i.e. we can no longer correctly
334# deserialize a previously serialized HostInfo. An example of such change is if
335# a field in the HostInfo object is dropped.
336_CURRENT_SERIALIZATION_VERSION = 1
337
338
339def json_serialize(info, file_obj, version=_CURRENT_SERIALIZATION_VERSION):
340    """Serialize the given HostInfo.
341
342    @param info: A HostInfo object to serialize.
343    @param file_obj: A file like object to serialize info into.
344    @param version: Use a specific serialization version. Should mostly use the
345            default.
346    """
347    info_json = {
348            'serializer_version': version,
349            'labels': info.labels,
350            'attributes': info.attributes,
351    }
352    return json.dump(info_json, file_obj, sort_keys=True, indent=4,
353                     separators=(',', ': '))
354
355
356def json_deserialize(file_obj):
357    """Deserialize a HostInfo from the given file.
358
359    @param file_obj: a file like object containing a json_serialized()ed
360            HostInfo.
361    @returns: The deserialized HostInfo object.
362    """
363    try:
364        deserialized_json = json.load(file_obj)
365    except ValueError as e:
366        raise DeserializationError(e)
367
368    serializer_version = deserialized_json.get('serializer_version')
369    if serializer_version != 1:
370        raise DeserializationError('Unsupported serialization version %s' %
371                                   serializer_version)
372
373    try:
374        return HostInfo(deserialized_json['labels'],
375                        deserialized_json['attributes'])
376    except KeyError as e:
377        raise DeserializationError('Malformed serialized host_info: %r' % e)
378
379
380def _to_label_prefix(prefix):
381    """Ensure that prefix has the expected format for label prefixes.
382
383    @param prefix: The (str) prefix to sanitize.
384    @returns: The sanitized (str) prefix.
385    """
386    return prefix if prefix.endswith(':') else prefix + ':'
387