# Copyright (c) 2013 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import logging import random from time import sleep import common from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib import utils from autotest_lib.server.cros.ap_configurators import \ ap_configurator_factory from autotest_lib.client.common_lib.cros.network import ap_constants from autotest_lib.server.cros.ap_configurators import ap_cartridge # Max number of retry attempts to lock an ap. MAX_RETRIES = 3 class ApLocker(object): """Object to keep track of AP lock state. @attribute configurator: an APConfigurator object. @attribute to_be_locked: a boolean, True iff ap has not been locked. @attribute retries: an integer, max number of retry attempts to lock ap. """ def __init__(self, configurator, retries): """Initialize. @param configurator: an APConfigurator object. @param retries: an integer, max number of retry attempts to lock ap. """ self.configurator = configurator self.to_be_locked = True self.retries = retries def __repr__(self): """@return class name, ap host name, lock status and retries.""" return 'class: %s, host name: %s, to_be_locked = %s, retries = %d' % ( self.__class__.__name__, self.configurator.host_name, self.to_be_locked, self.retries) def construct_ap_lockers(ap_spec, retries, hostname_matching_only=False, ap_test_type=ap_constants.AP_TEST_TYPE_CHAOS): """Convert APConfigurator objects to ApLocker objects for locking. @param ap_spec: an APSpec object @param retries: an integer, max number of retry attempts to lock ap. @param hostname_matching_only: a boolean, if True matching against all other APSpec parameters is not performed. @param ap_test_type: Used to determine which type of test we're currently running (Chaos vs Clique). @return a list of ApLocker objects. """ ap_lockers_list = [] factory = ap_configurator_factory.APConfiguratorFactory(ap_test_type, ap_spec) if hostname_matching_only: for ap in factory.get_aps_by_hostnames(ap_spec.hostnames): ap_lockers_list.append(ApLocker(ap, retries)) else: for ap in factory.get_ap_configurators_by_spec(ap_spec): ap_lockers_list.append(ApLocker(ap, retries)) if not len(ap_lockers_list): logging.error('Found no matching APs to test against for %s', ap_spec) logging.debug('Found %d APs', len(ap_lockers_list)) return ap_lockers_list class ApBatchLocker(object): """Object to lock/unlock an APConfigurator. @attribute SECONDS_TO_SLEEP: an integer, number of seconds to sleep between retries. @attribute ap_spec: an APSpec object @attribute retries: an integer, max number of retry attempts to lock ap. Defaults to MAX_RETRIES. @attribute aps_to_lock: a list of ApLocker objects. @attribute manager: a HostLockManager object, used to lock/unlock APs. """ MIN_SECONDS_TO_SLEEP = 10 MAX_SECONDS_TO_SLEEP = 20 def __init__(self, lock_manager, ap_spec, retries=MAX_RETRIES, hostname_matching_only=False, ap_test_type=ap_constants.AP_TEST_TYPE_CHAOS): """Initialize. @param ap_spec: an APSpec object @param retries: an integer, max number of retry attempts to lock ap. Defaults to MAX_RETRIES. @param hostname_matching_only : a boolean, if True matching against all other APSpec parameters is not performed. @param ap_test_type: Used to determine which type of test we're currently running (Chaos vs Clique). """ self.aps_to_lock = construct_ap_lockers(ap_spec, retries, hostname_matching_only=hostname_matching_only, ap_test_type=ap_test_type) self.manager = lock_manager self._locked_aps = [] def has_more_aps(self): """@return True iff there is at least one AP to be locked.""" return len(self.aps_to_lock) > 0 def lock_ap(self, ap_locker): """Locks an AP host in DataStore. @param ap_locker: an ApLocker object, AP to be locked. @return a boolean, True if ap_locker is locked. """ if self.manager.lock([ap_locker.configurator.host_name]): self._locked_aps.append(ap_locker) logging.info('locked %s', ap_locker.configurator.host_name) ap_locker.to_be_locked = False return True else: ap_locker.retries -= 1 logging.info('%d retries left for %s', ap_locker.retries, ap_locker.configurator.host_name) if ap_locker.retries == 0: logging.info('No more retries left. Remove %s from list', ap_locker.configurator.host_name) ap_locker.to_be_locked = False return False def get_ap_batch(self, batch_size=ap_cartridge.THREAD_MAX): """Allocates a batch of locked APs. @param batch_size: an integer, max. number of aps to lock in one batch. Defaults to THREAD_MAX in ap_cartridge.py @return a list of APConfigurator objects, locked in datastore. """ # We need this while loop to continuously loop over the for loop. # To exit the while loop, we either: # - locked batch_size number of aps and return them # - exhausted all retries on all aps in aps_to_lock while len(self.aps_to_lock): ap_batch = [] for ap_locker in self.aps_to_lock: logging.info('checking %s', ap_locker.configurator.host_name) # Lock AP in DataStore if self.lock_ap(ap_locker): ap_batch.append(ap_locker.configurator) if len(ap_batch) == batch_size: break # Remove locked APs from list of APs to process. aps_to_rm = [ap for ap in self.aps_to_lock if not ap.to_be_locked] self.aps_to_lock = list(set(self.aps_to_lock) - set(aps_to_rm)) for ap in aps_to_rm: logging.info('Removed %s from self.aps_to_lock', ap.configurator.host_name) logging.info('Remaining aps to lock = %s', [ap.configurator.host_name for ap in self.aps_to_lock]) # Return available APs and retry remaining ones later. if ap_batch: return ap_batch # Sleep before next retry. if self.aps_to_lock: seconds_to_sleep = random.randint(self.MIN_SECONDS_TO_SLEEP, self.MAX_SECONDS_TO_SLEEP) logging.info('Sleep %d sec before retry', seconds_to_sleep) sleep(seconds_to_sleep) return [] def unlock_one_ap(self, host_name): """ Unlock one AP from datastore after we're done. @param host_name: a string, AP host name. @raise TestError: when unable to unlock AP in datastore. """ for ap_locker in self._locked_aps: if host_name == ap_locker.configurator.host_name: self.manager.unlock(hosts=[host_name]) self._locked_aps.remove(ap_locker) return logging.error('Tried to unlock a host we have not locked (%s)?', host_name) def unlock_aps(self): """Unlock APs after we're done.""" # Make a copy of all of the hostnames to process host_names = list() for ap_locker in self._locked_aps: host_names.append(ap_locker.configurator.host_name) for host_name in host_names: self.unlock_one_ap(host_name) def unlock_and_reclaim_ap(self, host_name): """Unlock an AP but return it to the remaining batch of APs. @param host_name: a string, host name. """ for ap_locker in self._locked_aps: if host_name == ap_locker.configurator.host_name: self.aps_to_lock.append(ap_locker) self.unlock_one_ap(host_name) return def unlock_and_reclaim_aps(self): """Unlock APs but return them to the batch of remining APs. unlock_aps() will remove the remaining APs from the list of all APs to process. This method will add the remaining APs back to the pool of unprocessed APs. """ # Add the APs back into the pool self.aps_to_lock.extend(self._locked_aps) self.unlock_aps()