blob: 65d9a44a4f50ef535b5337aa1ce3397d59b1c6f0 [file] [log] [blame]
# 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
from autotest_lib.server.cros.chaos_lib import chaos_datastore_utils as dutils
# 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 = 30
MAX_SECONDS_TO_SLEEP = 120
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_in_afe(self, ap_locker):
"""Locks an AP host in AFE.
@param ap_locker: an ApLocker object, AP to be locked.
@return a boolean, True iff ap_locker is locked.
"""
if not utils.host_is_in_lab_zone(ap_locker.configurator.host_name):
ap_locker.to_be_locked = False
return True
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 lock_ap_in_datastore(self, ap_locker):
"""
Lock an AP host in datastore.
Test iterates through list of APs available in chaos_ap_list.conf file.
If AP with host_name is not found in datastore, this method adds the
same and shall use it for locking and testing.
@param ap_locker: an ApLocker object, AP to be locked.
@return a boolean, True iff ap_locker is locked.
"""
#ToDo dsunkara@: Find if below check is needed when not using Autotest.
if not utils.host_is_in_lab_zone(ap_locker.configurator.host_name):
ap_locker.to_be_locked = False
return True
# Get status of AP in datastore.
ap_device = dutils.show_device(ap_locker.configurator.host_name)
# Check if both Find / Add operations failed.
if not ap_device:
logging.error("Unable to find: %s in Datastore.",
ap_locker.configurator.host_name)
ap_locker.to_be_locked = False
# Check lock status of AP before trying to lock it.
# Lock AP if its not locked, else retry
elif ap_device['lock_status']:
logging.error("AP is already locked by %s at %s",
ap_device['locked_by'],
ap_device['lock_status_updated'])
ap_locker.to_be_locked = False
else:
# Lock device in datastore.
if dutils.lock_device(ap_locker.configurator.host_name):
self._locked_aps.append(ap_locker)
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 on AFE.
"""
# 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_in_datastore(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 after we're done.
@param host_name: a string, host name.
"""
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_one_ap_in_datastore(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:
# Unlock in datastore
# ToDo @dsunkara: change method name to unlock_one_ap
# once all dependencies are cleared on AFE.
unlocked_device = dutils.unlock_device(host_name)
if not unlocked_device:
raise error.TestError('Failed to unlock AP: %s',
host_name)
else:
self._locked_aps.remove(ap_locker)
return
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_in_datastore(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_in_datastore(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()