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