xref: /aosp_15_r20/external/autotest/server/hosts/base_label.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Copyright 2016 The Chromium OS Authors. All rights reserved.
2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
3*9c5db199SXin Li# found in the LICENSE file.
4*9c5db199SXin Li
5*9c5db199SXin Li"""This class defines the Base Label classes."""
6*9c5db199SXin Li
7*9c5db199SXin Li
8*9c5db199SXin Liimport logging
9*9c5db199SXin Li
10*9c5db199SXin Liimport common
11*9c5db199SXin Lifrom autotest_lib.server.hosts import afe_store
12*9c5db199SXin Lifrom autotest_lib.server.hosts import host_info
13*9c5db199SXin Lifrom autotest_lib.server.hosts import shadowing_store
14*9c5db199SXin Li
15*9c5db199SXin Li
16*9c5db199SXin Lidef forever_exists_decorate(exists):
17*9c5db199SXin Li    """
18*9c5db199SXin Li    Decorator for labels that should exist forever once applied.
19*9c5db199SXin Li
20*9c5db199SXin Li    We'll check if the label already exists on the host and return True if so.
21*9c5db199SXin Li    Otherwise we'll check if the label should exist on the host.
22*9c5db199SXin Li
23*9c5db199SXin Li    @param exists: The exists method on the label class.
24*9c5db199SXin Li    """
25*9c5db199SXin Li    def exists_wrapper(self, host):
26*9c5db199SXin Li        """
27*9c5db199SXin Li        Wrapper around the label exists method.
28*9c5db199SXin Li
29*9c5db199SXin Li        @param self: The label object.
30*9c5db199SXin Li        @param host: The host object to run methods on.
31*9c5db199SXin Li
32*9c5db199SXin Li        @returns True if the label already exists on the host, otherwise run
33*9c5db199SXin Li            the exists method.
34*9c5db199SXin Li        """
35*9c5db199SXin Li        info = host.host_info_store.get()
36*9c5db199SXin Li        return (self._NAME in info.labels) or exists(self, host)
37*9c5db199SXin Li    return exists_wrapper
38*9c5db199SXin Li
39*9c5db199SXin Li
40*9c5db199SXin Liclass BaseLabel(object):
41*9c5db199SXin Li    """
42*9c5db199SXin Li    This class contains the scaffolding for the host-specific labels.
43*9c5db199SXin Li
44*9c5db199SXin Li    @property _NAME String that is either the label returned or a prefix of a
45*9c5db199SXin Li                    generated label.
46*9c5db199SXin Li    """
47*9c5db199SXin Li
48*9c5db199SXin Li    _NAME = None
49*9c5db199SXin Li
50*9c5db199SXin Li    def generate_labels(self, host):
51*9c5db199SXin Li        """
52*9c5db199SXin Li        Return the list of labels generated for the host.
53*9c5db199SXin Li
54*9c5db199SXin Li        @param host: The host object to check on.  Not needed here for base case
55*9c5db199SXin Li                     but could be needed for subclasses.
56*9c5db199SXin Li
57*9c5db199SXin Li        @return a list of labels applicable to the host.
58*9c5db199SXin Li        """
59*9c5db199SXin Li        return [self._NAME]
60*9c5db199SXin Li
61*9c5db199SXin Li
62*9c5db199SXin Li    def exists(self, host):
63*9c5db199SXin Li        """
64*9c5db199SXin Li        Checks the host if the label is applicable or not.
65*9c5db199SXin Li
66*9c5db199SXin Li        This method is geared for the type of labels that indicate if the host
67*9c5db199SXin Li        has a feature (bluetooth, touchscreen, etc) and as such require
68*9c5db199SXin Li        detection logic to determine if the label should be applicable to the
69*9c5db199SXin Li        host or not.
70*9c5db199SXin Li
71*9c5db199SXin Li        @param host: The host object to check on.
72*9c5db199SXin Li        """
73*9c5db199SXin Li        raise NotImplementedError('exists not implemented')
74*9c5db199SXin Li
75*9c5db199SXin Li
76*9c5db199SXin Li    def get(self, host):
77*9c5db199SXin Li        """
78*9c5db199SXin Li        Return the list of labels.
79*9c5db199SXin Li
80*9c5db199SXin Li        @param host: The host object to check on.
81*9c5db199SXin Li        """
82*9c5db199SXin Li        if self.exists(host):
83*9c5db199SXin Li            return self.generate_labels(host)
84*9c5db199SXin Li        else:
85*9c5db199SXin Li            return []
86*9c5db199SXin Li
87*9c5db199SXin Li
88*9c5db199SXin Li    def get_all_labels(self):
89*9c5db199SXin Li        """
90*9c5db199SXin Li        Return all possible labels generated by this label class.
91*9c5db199SXin Li
92*9c5db199SXin Li        @returns a tuple of sets, the first set is for labels that are prefixes
93*9c5db199SXin Li            like 'os:android'.  The second set is for labels that are full
94*9c5db199SXin Li            labels by themselves like 'bluetooth'.
95*9c5db199SXin Li        """
96*9c5db199SXin Li        # Another subclass takes care of prefixed labels so this is empty.
97*9c5db199SXin Li        prefix_labels = set()
98*9c5db199SXin Li        full_labels_list = (self._NAME if isinstance(self._NAME, list) else
99*9c5db199SXin Li                            [self._NAME])
100*9c5db199SXin Li        full_labels = set(full_labels_list)
101*9c5db199SXin Li
102*9c5db199SXin Li        return prefix_labels, full_labels
103*9c5db199SXin Li
104*9c5db199SXin Li
105*9c5db199SXin Li    def update_for_task(self, task_name):
106*9c5db199SXin Li        """
107*9c5db199SXin Li        This method helps to check which labels need to be updated.
108*9c5db199SXin Li        State config labels are updated only for repair task.
109*9c5db199SXin Li        Lab config labels are updated only for deploy task.
110*9c5db199SXin Li        All labels are updated for any task.
111*9c5db199SXin Li
112*9c5db199SXin Li        It is the responsibility of the subclass to override this method
113*9c5db199SXin Li        to differentiate itself as a state config label or a lab config label
114*9c5db199SXin Li        and return the appropriate boolean value.
115*9c5db199SXin Li
116*9c5db199SXin Li        If the subclass doesn't override this method then that label will
117*9c5db199SXin Li        always be updated for any type of task.
118*9c5db199SXin Li
119*9c5db199SXin Li        @returns True if labels should be updated for the task with given name
120*9c5db199SXin Li        """
121*9c5db199SXin Li        return True
122*9c5db199SXin Li
123*9c5db199SXin Li
124*9c5db199SXin Liclass StringLabel(BaseLabel):
125*9c5db199SXin Li    """
126*9c5db199SXin Li    This class represents a string label that is dynamically generated.
127*9c5db199SXin Li
128*9c5db199SXin Li    This label class is used for the types of label that are always
129*9c5db199SXin Li    present and will return at least one label out of a list of possible labels
130*9c5db199SXin Li    (listed in _NAME).  It is required that the subclasses implement
131*9c5db199SXin Li    generate_labels() since the label class will need to figure out which labels
132*9c5db199SXin Li    to return.
133*9c5db199SXin Li
134*9c5db199SXin Li    _NAME must always be overridden by the subclass with all the possible
135*9c5db199SXin Li    labels that this label detection class can return in order to allow for
136*9c5db199SXin Li    accurate label updating.
137*9c5db199SXin Li    """
138*9c5db199SXin Li
139*9c5db199SXin Li    def generate_labels(self, host):
140*9c5db199SXin Li        raise NotImplementedError('generate_labels not implemented')
141*9c5db199SXin Li
142*9c5db199SXin Li
143*9c5db199SXin Li    def exists(self, host):
144*9c5db199SXin Li        """Set to true since it is assumed the label is always applicable."""
145*9c5db199SXin Li        return True
146*9c5db199SXin Li
147*9c5db199SXin Li
148*9c5db199SXin Liclass StringPrefixLabel(StringLabel):
149*9c5db199SXin Li    """
150*9c5db199SXin Li    This class represents a string label that is dynamically generated.
151*9c5db199SXin Li
152*9c5db199SXin Li    This label class is used for the types of label that usually are always
153*9c5db199SXin Li    present and indicate the os/board/etc type of the host.  The _NAME property
154*9c5db199SXin Li    will be prepended with a colon to the generated labels like so:
155*9c5db199SXin Li
156*9c5db199SXin Li        _NAME = 'os'
157*9c5db199SXin Li        generate_label() returns ['android']
158*9c5db199SXin Li
159*9c5db199SXin Li    The labels returned by this label class will be ['os:android'].
160*9c5db199SXin Li    It is important that the _NAME attribute be overridden by the
161*9c5db199SXin Li    subclass; otherwise, all labels returned will be prefixed with 'None:'.
162*9c5db199SXin Li    """
163*9c5db199SXin Li
164*9c5db199SXin Li    def get(self, host):
165*9c5db199SXin Li        """Return the list of labels with _NAME prefixed with a colon.
166*9c5db199SXin Li
167*9c5db199SXin Li        @param host: The host object to check on.
168*9c5db199SXin Li        """
169*9c5db199SXin Li        if self.exists(host):
170*9c5db199SXin Li            return ['%s:%s' % (self._NAME, label)
171*9c5db199SXin Li                    for label in self.generate_labels(host)]
172*9c5db199SXin Li        else:
173*9c5db199SXin Li            return []
174*9c5db199SXin Li
175*9c5db199SXin Li
176*9c5db199SXin Li    def get_all_labels(self):
177*9c5db199SXin Li        """
178*9c5db199SXin Li        Return all possible labels generated by this label class.
179*9c5db199SXin Li
180*9c5db199SXin Li        @returns a tuple of sets, the first set is for labels that are prefixes
181*9c5db199SXin Li            like 'os:android'.  The second set is for labels that are full
182*9c5db199SXin Li            labels by themselves like 'bluetooth'.
183*9c5db199SXin Li        """
184*9c5db199SXin Li        # Since this is a prefix label class, we only care about
185*9c5db199SXin Li        # prefixed_labels.  We'll need to append the ':' to the label name to
186*9c5db199SXin Li        # make sure we only match on prefix labels.
187*9c5db199SXin Li        full_labels = set()
188*9c5db199SXin Li        prefix_labels = set(['%s:' % self._NAME])
189*9c5db199SXin Li
190*9c5db199SXin Li        return prefix_labels, full_labels
191*9c5db199SXin Li
192*9c5db199SXin Li
193*9c5db199SXin Liclass LabelRetriever(object):
194*9c5db199SXin Li    """This class will assist in retrieving/updating the host labels."""
195*9c5db199SXin Li
196*9c5db199SXin Li    def _populate_known_labels(self, label_list, task_name):
197*9c5db199SXin Li        """Create a list of known labels that is created through this class."""
198*9c5db199SXin Li        for label_instance in label_list:
199*9c5db199SXin Li            # populate only the labels that need to be updated for this task.
200*9c5db199SXin Li            if label_instance.update_for_task(task_name):
201*9c5db199SXin Li                prefixed_labels, full_labels = label_instance.get_all_labels()
202*9c5db199SXin Li                self.label_prefix_names.update(prefixed_labels)
203*9c5db199SXin Li                self.label_full_names.update(full_labels)
204*9c5db199SXin Li
205*9c5db199SXin Li
206*9c5db199SXin Li    def __init__(self, label_list):
207*9c5db199SXin Li        self._labels = label_list
208*9c5db199SXin Li        # These two sets will contain the list of labels we can safely remove
209*9c5db199SXin Li        # during the update_labels call.
210*9c5db199SXin Li        self.label_full_names = set()
211*9c5db199SXin Li        self.label_prefix_names = set()
212*9c5db199SXin Li
213*9c5db199SXin Li
214*9c5db199SXin Li    def get_labels(self, host):
215*9c5db199SXin Li        """
216*9c5db199SXin Li        Retrieve the labels for the host.
217*9c5db199SXin Li
218*9c5db199SXin Li        @param host: The host to get the labels for.
219*9c5db199SXin Li        """
220*9c5db199SXin Li        labels = []
221*9c5db199SXin Li        for label in self._labels:
222*9c5db199SXin Li            logging.info('checking label %s', label.__class__.__name__)
223*9c5db199SXin Li            try:
224*9c5db199SXin Li                labels.extend(label.get(host))
225*9c5db199SXin Li            except Exception:
226*9c5db199SXin Li                logging.exception('error getting label %s.',
227*9c5db199SXin Li                                  label.__class__.__name__)
228*9c5db199SXin Li        return labels
229*9c5db199SXin Li
230*9c5db199SXin Li
231*9c5db199SXin Li    def get_labels_for_update(self, host, task_name):
232*9c5db199SXin Li        """
233*9c5db199SXin Li        Retrieve the labels for the host which needs to be updated.
234*9c5db199SXin Li
235*9c5db199SXin Li        @param host: The host to get the labels for updating.
236*9c5db199SXin Li        @param task_name: task name(repair/deploy) for the operation.
237*9c5db199SXin Li
238*9c5db199SXin Li        @returns labels to be updated
239*9c5db199SXin Li        """
240*9c5db199SXin Li        labels = []
241*9c5db199SXin Li        for label in self._labels:
242*9c5db199SXin Li            try:
243*9c5db199SXin Li                # get only the labels which need to be updated for this task.
244*9c5db199SXin Li                if label.update_for_task(task_name):
245*9c5db199SXin Li                    logging.info('checking label update %s',
246*9c5db199SXin Li                                 label.__class__.__name__)
247*9c5db199SXin Li                    labels.extend(label.get(host))
248*9c5db199SXin Li            except Exception:
249*9c5db199SXin Li                logging.exception('error getting label %s.',
250*9c5db199SXin Li                                  label.__class__.__name__)
251*9c5db199SXin Li        return labels
252*9c5db199SXin Li
253*9c5db199SXin Li
254*9c5db199SXin Li    def _is_known_label(self, label):
255*9c5db199SXin Li        """
256*9c5db199SXin Li        Checks if the label is a label known to the label detection framework.
257*9c5db199SXin Li
258*9c5db199SXin Li        @param label: The label to check if we want to skip or not.
259*9c5db199SXin Li
260*9c5db199SXin Li        @returns True to skip (which means to keep this label, False to remove.
261*9c5db199SXin Li        """
262*9c5db199SXin Li        return (label in self.label_full_names or
263*9c5db199SXin Li                any([label.startswith(p) for p in self.label_prefix_names]))
264*9c5db199SXin Li
265*9c5db199SXin Li
266*9c5db199SXin Li    def _carry_over_unknown_labels(self, old_labels, new_labels):
267*9c5db199SXin Li        """Update new_labels by adding back old unknown labels.
268*9c5db199SXin Li
269*9c5db199SXin Li        We only delete labels that we might have created earlier.  There are
270*9c5db199SXin Li        some labels we should not be removing (e.g. pool:bvt) that we
271*9c5db199SXin Li        want to keep but won't be part of the new labels detected on the host.
272*9c5db199SXin Li        To do that we compare the passed in label to our list of known labels
273*9c5db199SXin Li        and if we get a match, we feel safe knowing we can remove the label.
274*9c5db199SXin Li        Otherwise we leave that label alone since it was generated elsewhere.
275*9c5db199SXin Li
276*9c5db199SXin Li        @param old_labels: List of labels already on the host.
277*9c5db199SXin Li        @param new_labels: List of newly detected labels. This list will be
278*9c5db199SXin Li                updated to add back labels that are not tracked by the detection
279*9c5db199SXin Li                framework.
280*9c5db199SXin Li        """
281*9c5db199SXin Li        missing_labels = set(old_labels) - set(new_labels)
282*9c5db199SXin Li        for label in missing_labels:
283*9c5db199SXin Li            if not self._is_known_label(label):
284*9c5db199SXin Li                new_labels.append(label)
285*9c5db199SXin Li
286*9c5db199SXin Li
287*9c5db199SXin Li    def _commit_info(self, host, new_info, keep_pool):
288*9c5db199SXin Li        if keep_pool and isinstance(host.host_info_store,
289*9c5db199SXin Li                                    shadowing_store.ShadowingStore):
290*9c5db199SXin Li            primary_store = afe_store.AfeStoreKeepPool(host.hostname)
291*9c5db199SXin Li            host.host_info_store.commit_with_substitute(
292*9c5db199SXin Li                    new_info,
293*9c5db199SXin Li                    primary_store=primary_store,
294*9c5db199SXin Li                    shadow_store=None)
295*9c5db199SXin Li            return
296*9c5db199SXin Li
297*9c5db199SXin Li        host.host_info_store.commit(new_info)
298*9c5db199SXin Li
299*9c5db199SXin Li
300*9c5db199SXin Li    def update_labels(self, host, task_name='', keep_pool=False):
301*9c5db199SXin Li        """
302*9c5db199SXin Li        Retrieve the labels from the host and update if needed.
303*9c5db199SXin Li
304*9c5db199SXin Li        @param host: The host to update the labels for.
305*9c5db199SXin Li        """
306*9c5db199SXin Li        # If we haven't yet grabbed our list of known labels, do so now.
307*9c5db199SXin Li        if not self.label_full_names and not self.label_prefix_names:
308*9c5db199SXin Li            self._populate_known_labels(self._labels, task_name)
309*9c5db199SXin Li
310*9c5db199SXin Li        # Label detection hits the DUT so it can be slow. Do it before reading
311*9c5db199SXin Li        # old labels from HostInfoStore to minimize the time between read and
312*9c5db199SXin Li        # commit of the HostInfo.
313*9c5db199SXin Li        new_labels = self.get_labels_for_update(host, task_name)
314*9c5db199SXin Li        old_info = host.host_info_store.get()
315*9c5db199SXin Li        self._carry_over_unknown_labels(old_info.labels, new_labels)
316*9c5db199SXin Li        new_info = host_info.HostInfo(
317*9c5db199SXin Li                labels=new_labels,
318*9c5db199SXin Li                attributes=old_info.attributes,
319*9c5db199SXin Li                stable_versions=old_info.stable_versions,
320*9c5db199SXin Li        )
321*9c5db199SXin Li        if old_info != new_info:
322*9c5db199SXin Li            self._commit_info(host, new_info, keep_pool)
323