• 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
5"""This class defines the Base Label classes."""
6
7
8import logging
9
10import common
11from autotest_lib.server.hosts import afe_store
12from autotest_lib.server.hosts import host_info
13from autotest_lib.server.hosts import shadowing_store
14
15
16def 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
40class 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
105class 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
129class 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
174class 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