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