# Copyright 2015 The Chromium OS 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 collections import logging import random from time import sleep import common from autotest_lib.client.common_lib import error from autotest_lib.server import hosts from autotest_lib.server import frontend from autotest_lib.server import site_utils from autotest_lib.server.cros.dynamic_suite import constants from autotest_lib.server.cros.network import wifi_client # Max number of retry attempts to lock a DUT. MAX_RETRIES = 3 # Tuple containing the DUT objects DUTObject = collections.namedtuple('DUTObject', ['host', 'wifi_client']) class DUTSpec(): """Object to specify the DUT spec. @attribute board_name: String representing the board name corresponding to the board. @attribute host_name: String representing the host name corresponding to the machine. """ def __init__(self, board_name=None, host_name=None): """Initialize. @param board_name: String representing the board name corresponding to the board. @param host_name: String representing the host name corresponding to the machine. """ self.board_name = board_name self.host_name = host_name def __repr__(self): """@return class name, dut host name, lock status and retries.""" return 'class: %s, Board name: %s, Num DUTs = %d' % ( self.__class__.__name__, self.board_name, self.host_name) class DUTSetSpec(list): """Object to specify the DUT set spec. It's a list of DUTSpec objects.""" def __init__(self): """Initialize.""" super(DUTSetSpec, self) class DUTPoolSpec(list): """Object to specify the DUT pool spec.It's a list of DUTSetSpec objects.""" def __init__(self): """Initialize.""" super(DUTPoolSpec, self) class DUTLocker(object): """Object to keep track of DUT lock state. @attribute dut_spec: an DUTSpec object corresponding to the DUT we need. @attribute retries: an integer, max number of retry attempts to lock DUT. @attribute to_be_locked: a boolean, True iff DUT has not been locked. """ def __init__(self, dut_spec, retries): """Initialize. @param dut_spec: a DUTSpec object corresponding to the spec of the DUT to be locked. @param retries: an integer, max number of retry attempts to lock DUT. """ self.dut_spec = dut_spec self.retries = retries self.to_be_locked = True def __repr__(self): """@return class name, dut host name, lock status and retries.""" return 'class: %s, host name: %s, to_be_locked = %s, retries = %d' % ( self.__class__.__name__, self.dut.host.hostname, self.to_be_locked, self.retries) class CliqueDUTBatchLocker(object): """Object to lock/unlock an DUT. @attribute SECONDS_TO_SLEEP: an integer, number of seconds to sleep between retries. @attribute duts_to_lock: a list of DUTLocker objects. @attribute locked_duts: a list of DUTObject's corresponding to DUT's which have already been allocated. @attribute manager: a HostLockManager object, used to lock/unlock DUTs. """ MIN_SECONDS_TO_SLEEP = 30 MAX_SECONDS_TO_SLEEP = 120 def __init__(self, lock_manager, dut_pool_spec, retries=MAX_RETRIES): """Initialize. @param lock_manager: a HostLockManager object, used to lock/unlock DUTs. @param dut_pool_spec: A DUTPoolSpec object corresponding to the DUT's in the pool. @param retries: Number of times to retry the locking of DUT's. """ self.lock_manager = lock_manager self.duts_to_lock = self._construct_dut_lockers(dut_pool_spec, retries) self.locked_duts = [] @staticmethod def _construct_dut_lockers(dut_pool_spec, retries): """Convert DUTObject objects to DUTLocker objects for locking. @param dut_pool_spec: A DUTPoolSpec object corresponding to the DUT's in the pool. @param retries: an integer, max number of retry attempts to lock DUT. @return a list of DUTLocker objects. """ dut_lockers_list = [] for dut_set_spec in dut_pool_spec: dut_set_lockers_list = [] for dut_spec in dut_set_spec: dut_locker = DUTLocker(dut_spec, retries) dut_set_lockers_list.append(dut_locker) dut_lockers_list.append(dut_set_lockers_list) return dut_lockers_list def _allocate_dut(self, host_name=None, board_name=None): """Allocates a machine to the DUT pool for running the test. Locks the allocated machine if the machine was discovered via AFE to prevent tests stomping on each other. @param host_name: Host name for the DUT. @param board_name: Board name Label to use for finding the DUT. @return: hostname of the device locked in AFE. """ hostname = None if host_name: if self.lock_manager.lock([host_name]): logging.info('Locked device %s.', host_name) hostname = host_name else: logging.error('Unable to lock device %s.', host_name) else: afe = frontend.AFE(debug=True, server=site_utils.get_global_afe_hostname()) labels = [] labels.append(constants.BOARD_PREFIX + board_name) labels.append('clique_dut') try: hostname = site_utils.lock_host_with_labels( afe, self.lock_manager, labels=labels) + '.cros' except error.NoEligibleHostException as e: raise error.TestError("Unable to find a suitable device.") except error.TestError as e: logging.error(e) return hostname @staticmethod def _create_dut_object(host_name): """Create the DUTObject tuple for the DUT. @param host_name: Host name for the DUT. @return: Tuple of Host and Wifi client objects representing DUTObject for invoking RPC calls. """ dut_host = hosts.create_host(host_name) dut_wifi_client = wifi_client.WiFiClient(dut_host, './debug', False) return DUTObject(dut_host, dut_wifi_client) def _lock_dut_in_afe(self, dut_locker): """Locks an DUT host in AFE. @param dut_locker: an DUTLocker object, DUT to be locked. @return a hostname iff dut_locker is locked, else returns None. """ logging.debug('Trying to find a device with spec (%s, %s)', dut_locker.dut_spec.host_name, dut_locker.dut_spec.board_name) host_name = self._allocate_dut( dut_locker.dut_spec.host_name, dut_locker.dut_spec.board_name) if host_name: logging.info('Locked %s', host_name) dut_locker.to_be_locked = False else: dut_locker.retries -= 1 logging.info('%d retries left for (%s, %s)', dut_locker.retries, dut_locker.dut_spec.host_name, dut_locker.dut_spec.board_name) if dut_locker.retries == 0: raise error.TestError("No more retries left to lock a " "suitable device.") return host_name def get_dut_pool(self): """Allocates a batch of locked DUTs for the test. @return a list of DUTObject, 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 duts and return them # - exhausted all retries on a dut in duts_to_lock # It is important to preserve the order of DUT sets, but the order of # DUT's within the set is not important as all the DUT's within a set # have to perform the same role. dut_pool = [] for dut_set in self.duts_to_lock: dut_pool.append([]) num_duts_to_lock = sum(map(len, self.duts_to_lock)) while num_duts_to_lock: set_num = 0 for dut_locker_set in self.duts_to_lock: for dut_locker in dut_locker_set: if dut_locker.to_be_locked: host_name = self._lock_dut_in_afe(dut_locker) if host_name: dut_object = self._create_dut_object(host_name) self.locked_duts.append(dut_object) dut_pool[set_num].append(dut_object) num_duts_to_lock -= 1 set_num += 1 logging.info('Remaining DUTs to lock: %d', num_duts_to_lock) if num_duts_to_lock: seconds_to_sleep = random.randint(self.MIN_SECONDS_TO_SLEEP, self.MAX_SECONDS_TO_SLEEP) logging.debug('Sleep %d sec before retry', seconds_to_sleep) sleep(seconds_to_sleep) return dut_pool def _unlock_one_dut(self, dut): """Unlock one DUT after we're done. @param dut: a DUTObject corresponding to the DUT. """ host_name = dut.host.host_name if self.manager.unlock(hosts=[host_name]): self._locked_duts.remove(dut) else: logging.error('Tried to unlock a host we have not locked (%s)?', host_name) def unlock_duts(self): """Unlock DUTs after we're done.""" for dut in self.locked_duts: self._unlock_one_dut(dut) def unlock_and_close_duts(self): """Unlock DUTs after we're done and close the associated WifiClient.""" for dut in self.locked_duts: dut.wifi_client.close() self._unlock_one_dut(dut)