1# Copyright (c) 2013 The Chromium 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 logging 6import random 7import requests 8 9from time import sleep 10 11import common 12from autotest_lib.client.common_lib import utils 13from autotest_lib.server.cros.ap_configurators import \ 14 ap_configurator_factory 15from autotest_lib.client.common_lib.cros.network import ap_constants 16from autotest_lib.server.cros.ap_configurators import ap_cartridge 17 18 19# Max number of retry attempts to lock an ap. 20MAX_RETRIES = 3 21CHAOS_URL = 'https://chaos-188802.appspot.com' 22 23 24class ApLocker(object): 25 """Object to keep track of AP lock state. 26 27 @attribute configurator: an APConfigurator object. 28 @attribute to_be_locked: a boolean, True iff ap has not been locked. 29 @attribute retries: an integer, max number of retry attempts to lock ap. 30 """ 31 32 33 def __init__(self, configurator, retries): 34 """Initialize. 35 36 @param configurator: an APConfigurator object. 37 @param retries: an integer, max number of retry attempts to lock ap. 38 """ 39 self.configurator = configurator 40 self.to_be_locked = True 41 self.retries = retries 42 43 44 def __repr__(self): 45 """@return class name, ap host name, lock status and retries.""" 46 return 'class: %s, host name: %s, to_be_locked = %s, retries = %d' % ( 47 self.__class__.__name__, 48 self.configurator.host_name, 49 self.to_be_locked, 50 self.retries) 51 52 53def construct_ap_lockers(ap_spec, retries, hostname_matching_only=False, 54 ap_test_type=ap_constants.AP_TEST_TYPE_CHAOS): 55 """Convert APConfigurator objects to ApLocker objects for locking. 56 57 @param ap_spec: an APSpec object 58 @param retries: an integer, max number of retry attempts to lock ap. 59 @param hostname_matching_only: a boolean, if True matching against 60 all other APSpec parameters is not 61 performed. 62 @param ap_test_type: Used to determine which type of test we're 63 currently running (Chaos vs Clique). 64 65 @return a list of ApLocker objects. 66 """ 67 ap_lockers_list = [] 68 factory = ap_configurator_factory.APConfiguratorFactory(ap_test_type, 69 ap_spec) 70 if hostname_matching_only: 71 for ap in factory.get_aps_by_hostnames(ap_spec.hostnames): 72 ap_lockers_list.append(ApLocker(ap, retries)) 73 else: 74 for ap in factory.get_ap_configurators_by_spec(ap_spec): 75 ap_lockers_list.append(ApLocker(ap, retries)) 76 77 if not len(ap_lockers_list): 78 logging.error('Found no matching APs to test against for %s', ap_spec) 79 80 logging.debug('Found %d APs', len(ap_lockers_list)) 81 return ap_lockers_list 82 83 84class ApBatchLocker(object): 85 """Object to lock/unlock an APConfigurator. 86 87 @attribute SECONDS_TO_SLEEP: an integer, number of seconds to sleep between 88 retries. 89 @attribute ap_spec: an APSpec object 90 @attribute retries: an integer, max number of retry attempts to lock ap. 91 Defaults to MAX_RETRIES. 92 @attribute aps_to_lock: a list of ApLocker objects. 93 @attribute manager: a HostLockManager object, used to lock/unlock APs. 94 """ 95 96 97 MIN_SECONDS_TO_SLEEP = 30 98 MAX_SECONDS_TO_SLEEP = 120 99 100 101 def __init__(self, lock_manager, ap_spec, retries=MAX_RETRIES, 102 hostname_matching_only=False, 103 ap_test_type=ap_constants.AP_TEST_TYPE_CHAOS): 104 """Initialize. 105 106 @param ap_spec: an APSpec object 107 @param retries: an integer, max number of retry attempts to lock ap. 108 Defaults to MAX_RETRIES. 109 @param hostname_matching_only : a boolean, if True matching against 110 all other APSpec parameters is not 111 performed. 112 @param ap_test_type: Used to determine which type of test we're 113 currently running (Chaos vs Clique). 114 """ 115 self.aps_to_lock = construct_ap_lockers(ap_spec, retries, 116 hostname_matching_only=hostname_matching_only, 117 ap_test_type=ap_test_type) 118 self.manager = lock_manager 119 self._locked_aps = [] 120 121 122 def has_more_aps(self): 123 """@return True iff there is at least one AP to be locked.""" 124 return len(self.aps_to_lock) > 0 125 126 127 def lock_ap_in_afe(self, ap_locker): 128 """Locks an AP host in AFE. 129 130 @param ap_locker: an ApLocker object, AP to be locked. 131 @return a boolean, True iff ap_locker is locked. 132 """ 133 if not utils.host_is_in_lab_zone(ap_locker.configurator.host_name): 134 ap_locker.to_be_locked = False 135 return True 136 137 if self.manager.lock([ap_locker.configurator.host_name]): 138 self._locked_aps.append(ap_locker) 139 logging.info('locked %s', ap_locker.configurator.host_name) 140 ap_locker.to_be_locked = False 141 return True 142 else: 143 ap_locker.retries -= 1 144 logging.info('%d retries left for %s', 145 ap_locker.retries, 146 ap_locker.configurator.host_name) 147 if ap_locker.retries == 0: 148 logging.info('No more retries left. Remove %s from list', 149 ap_locker.configurator.host_name) 150 ap_locker.to_be_locked = False 151 152 return False 153 154 def lock_ap_in_datastore(self, ap_locker): 155 """Locks an AP host in datastore. 156 157 @param ap_locker: an ApLocker object, AP to be locked. 158 @return a boolean, True iff ap_locker is locked. 159 """ 160 if not utils.host_is_in_lab_zone(ap_locker.configurator.host_name): 161 ap_locker.to_be_locked = False 162 return True 163 164 # Begin locking device in datastore. 165 locked_device = requests.put(CHAOS_URL + '/devices/lock', \ 166 json={"hostname":[ap_locker.configurator.host_name], \ 167 "locked_by":"TestRun"}) 168 if locked_device.json()['result']: 169 self._locked_aps.append(ap_locker) 170 logging.info('locked %s', ap_locker.configurator.host_name) 171 ap_locker.to_be_locked = False 172 return True 173 else: 174 ap_locker.retries -= 1 175 logging.info('%d retries left for %s', 176 ap_locker.retries, 177 ap_locker.configurator.host_name) 178 if ap_locker.retries == 0: 179 logging.info('No more retries left. Remove %s from list', 180 ap_locker.configurator.host_name) 181 ap_locker.to_be_locked = False 182 183 return False 184 185 186 def get_ap_batch(self, batch_size=ap_cartridge.THREAD_MAX): 187 """Allocates a batch of locked APs. 188 189 @param batch_size: an integer, max. number of aps to lock in one batch. 190 Defaults to THREAD_MAX in ap_cartridge.py 191 @return a list of APConfigurator objects, locked on AFE. 192 """ 193 # We need this while loop to continuously loop over the for loop. 194 # To exit the while loop, we either: 195 # - locked batch_size number of aps and return them 196 # - exhausted all retries on all aps in aps_to_lock 197 while len(self.aps_to_lock): 198 ap_batch = [] 199 200 for ap_locker in self.aps_to_lock: 201 logging.info('checking %s', ap_locker.configurator.host_name) 202 # TODO(@rjahagir): Change method to datastore. 203 # if self.lock_ap_in_datastore(ap_locker): 204 if self.lock_ap_in_afe(ap_locker): 205 ap_batch.append(ap_locker.configurator) 206 if len(ap_batch) == batch_size: 207 break 208 209 # Remove locked APs from list of APs to process. 210 aps_to_rm = [ap for ap in self.aps_to_lock if not ap.to_be_locked] 211 self.aps_to_lock = list(set(self.aps_to_lock) - set(aps_to_rm)) 212 for ap in aps_to_rm: 213 logging.info('Removed %s from self.aps_to_lock', 214 ap.configurator.host_name) 215 logging.info('Remaining aps to lock = %s', 216 [ap.configurator.host_name for ap in self.aps_to_lock]) 217 218 # Return available APs and retry remaining ones later. 219 if ap_batch: 220 return ap_batch 221 222 # Sleep before next retry. 223 if self.aps_to_lock: 224 seconds_to_sleep = random.randint(self.MIN_SECONDS_TO_SLEEP, 225 self.MAX_SECONDS_TO_SLEEP) 226 logging.info('Sleep %d sec before retry', seconds_to_sleep) 227 sleep(seconds_to_sleep) 228 229 return [] 230 231 232 def unlock_one_ap(self, host_name): 233 """Unlock one AP after we're done. 234 235 @param host_name: a string, host name. 236 """ 237 for ap_locker in self._locked_aps: 238 if host_name == ap_locker.configurator.host_name: 239 self.manager.unlock(hosts=[host_name]) 240 self._locked_aps.remove(ap_locker) 241 return 242 243 logging.error('Tried to unlock a host we have not locked (%s)?', 244 host_name) 245 246 def unlock_one_ap_in_datastore(self, host_name): 247 """Unlock one AP from datastore after we're done. 248 249 @param host_name: a string, host name. 250 """ 251 for ap_locker in self._locked_aps: 252 if host_name == ap_locker.configurator.host_name: 253 # Unlock in datastore 254 unlocked_device = requests.put(CHAOS_URL + '/devices/unlock', \ 255 json={"hostname":host_name}) 256 # TODO: Raise error if unable to unlock. 257 if !unlocked_device.json()['result']: 258 raise error 259 logging.debug(unlocked_device.content()) 260 else: 261 self._locked_aps.remove(ap_locker) 262 return 263 264 logging.error('Tried to unlock a host we have not locked (%s)?', 265 host_name) 266 267 268 def unlock_aps(self): 269 """Unlock APs after we're done.""" 270 # Make a copy of all of the hostnames to process 271 host_names = list() 272 for ap_locker in self._locked_aps: 273 host_names.append(ap_locker.configurator.host_name) 274 for host_name in host_names: 275 # TODO(@rjahagir): Change method to datastore. 276 # self.unlock_one_ap_in_datastore(host_name) 277 self.unlock_one_ap(host_name) 278 279 280 def unlock_and_reclaim_ap(self, host_name): 281 """Unlock an AP but return it to the remaining batch of APs. 282 283 @param host_name: a string, host name. 284 """ 285 for ap_locker in self._locked_aps: 286 if host_name == ap_locker.configurator.host_name: 287 self.aps_to_lock.append(ap_locker) 288 # TODO(@rjahagir): Change method to datastore. 289 # self.unlock_one_ap_in_datastore(host_name) 290 self.unlock_one_ap(host_name) 291 return 292 293 294 def unlock_and_reclaim_aps(self): 295 """Unlock APs but return them to the batch of remining APs. 296 297 unlock_aps() will remove the remaining APs from the list of all APs 298 to process. This method will add the remaining APs back to the pool 299 of unprocessed APs. 300 301 """ 302 # Add the APs back into the pool 303 self.aps_to_lock.extend(self._locked_aps) 304 self.unlock_aps() 305