1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Liimport logging 7*9c5db199SXin Liimport signal 8*9c5db199SXin Lifrom . import common 9*9c5db199SXin Li 10*9c5db199SXin Lifrom autotest_lib.server import site_utils 11*9c5db199SXin Lifrom autotest_lib.server.cros.chaos_lib import chaos_datastore_utils 12*9c5db199SXin Li"""HostLockManager class, for the dynamic_suite module. 13*9c5db199SXin Li 14*9c5db199SXin LiA HostLockManager instance manages locking and unlocking a set of autotest DUTs. 15*9c5db199SXin LiA caller can lock or unlock one or more DUTs. If the caller fails to unlock() 16*9c5db199SXin Lilocked hosts before the instance is destroyed, it will attempt to unlock() the 17*9c5db199SXin Lihosts automatically, but this is to be avoided. 18*9c5db199SXin Li 19*9c5db199SXin LiSample usage: 20*9c5db199SXin Li manager = host_lock_manager.HostLockManager() 21*9c5db199SXin Li try: 22*9c5db199SXin Li manager.lock(['host1']) 23*9c5db199SXin Li # do things 24*9c5db199SXin Li finally: 25*9c5db199SXin Li manager.unlock() 26*9c5db199SXin Li""" 27*9c5db199SXin Li 28*9c5db199SXin Liclass HostLockManager(object): 29*9c5db199SXin Li """ 30*9c5db199SXin Li @attribute _afe: an instance of AFE as defined in server/frontend.py. 31*9c5db199SXin Li @attribute _locked_hosts: a set of DUT hostnames. 32*9c5db199SXin Li @attribute LOCK: a string. 33*9c5db199SXin Li @attribute UNLOCK: a string. 34*9c5db199SXin Li """ 35*9c5db199SXin Li 36*9c5db199SXin Li LOCK = 'lock' 37*9c5db199SXin Li UNLOCK = 'unlock' 38*9c5db199SXin Li 39*9c5db199SXin Li 40*9c5db199SXin Li @property 41*9c5db199SXin Li def locked_hosts(self): 42*9c5db199SXin Li """@returns set of locked hosts.""" 43*9c5db199SXin Li return self._locked_hosts 44*9c5db199SXin Li 45*9c5db199SXin Li 46*9c5db199SXin Li @locked_hosts.setter 47*9c5db199SXin Li def locked_hosts(self, hosts): 48*9c5db199SXin Li """Sets value of locked_hosts. 49*9c5db199SXin Li 50*9c5db199SXin Li @param hosts: a set of strings. 51*9c5db199SXin Li """ 52*9c5db199SXin Li self._locked_hosts = hosts 53*9c5db199SXin Li 54*9c5db199SXin Li 55*9c5db199SXin Li def __init__(self, afe=None): 56*9c5db199SXin Li """ 57*9c5db199SXin Li Constructor 58*9c5db199SXin Li """ 59*9c5db199SXin Li self.dutils = chaos_datastore_utils.ChaosDataStoreUtils() 60*9c5db199SXin Li # Keep track of hosts locked by this instance. 61*9c5db199SXin Li self._locked_hosts = set() 62*9c5db199SXin Li 63*9c5db199SXin Li 64*9c5db199SXin Li def __del__(self): 65*9c5db199SXin Li if self._locked_hosts: 66*9c5db199SXin Li logging.warning('Caller failed to unlock %r! Forcing unlock now.', 67*9c5db199SXin Li self._locked_hosts) 68*9c5db199SXin Li self.unlock() 69*9c5db199SXin Li 70*9c5db199SXin Li 71*9c5db199SXin Li def _check_host(self, host, operation): 72*9c5db199SXin Li """Checks host for desired operation. 73*9c5db199SXin Li 74*9c5db199SXin Li @param host: a string, hostname. 75*9c5db199SXin Li @param operation: a string, LOCK or UNLOCK. 76*9c5db199SXin Li @returns a string: host name, if desired operation can be performed on 77*9c5db199SXin Li host or None otherwise. 78*9c5db199SXin Li """ 79*9c5db199SXin Li host_checked = host 80*9c5db199SXin Li # Get host details from DataStore 81*9c5db199SXin Li host_info = self.dutils.show_device(host) 82*9c5db199SXin Li 83*9c5db199SXin Li if not host_info: 84*9c5db199SXin Li logging.warning('Host (AP) details not found in DataStore') 85*9c5db199SXin Li return None 86*9c5db199SXin Li 87*9c5db199SXin Li if operation == self.LOCK and host_info['lock_status']: 88*9c5db199SXin Li err = ('Contention detected: %s is locked by %s at %s.' % 89*9c5db199SXin Li (host, host_info['locked_by'], 90*9c5db199SXin Li host_info['lock_status_updated'])) 91*9c5db199SXin Li logging.error(err) 92*9c5db199SXin Li return None 93*9c5db199SXin Li 94*9c5db199SXin Li elif operation == self.UNLOCK and not host_info['lock_status']: 95*9c5db199SXin Li logging.info('%s not locked.', host) 96*9c5db199SXin Li return None 97*9c5db199SXin Li 98*9c5db199SXin Li return host_checked 99*9c5db199SXin Li 100*9c5db199SXin Li 101*9c5db199SXin Li def lock(self, hosts, lock_reason='Locked by HostLockManager'): 102*9c5db199SXin Li """Lock hosts in datastore. 103*9c5db199SXin Li 104*9c5db199SXin Li @param hosts: a list of strings, host names. 105*9c5db199SXin Li @param lock_reason: a string, a reason for locking the hosts. 106*9c5db199SXin Li 107*9c5db199SXin Li @returns a boolean, True == at least one host from hosts is locked. 108*9c5db199SXin Li """ 109*9c5db199SXin Li # Filter out hosts that we may have already locked 110*9c5db199SXin Li new_hosts = set(hosts).difference(self._locked_hosts) 111*9c5db199SXin Li logging.info('Attempt to lock %s', new_hosts) 112*9c5db199SXin Li if not new_hosts: 113*9c5db199SXin Li return False 114*9c5db199SXin Li 115*9c5db199SXin Li return self._host_modifier(new_hosts, self.LOCK, lock_reason=lock_reason) 116*9c5db199SXin Li 117*9c5db199SXin Li 118*9c5db199SXin Li def unlock(self, hosts=None): 119*9c5db199SXin Li """Unlock hosts in datastore after use. 120*9c5db199SXin Li 121*9c5db199SXin Li @param hosts: a list of strings, host names. 122*9c5db199SXin Li @returns a boolean, True == at least one host from self._locked_hosts is 123*9c5db199SXin Li unlocked. 124*9c5db199SXin Li """ 125*9c5db199SXin Li # Filter out hosts that we did not lock 126*9c5db199SXin Li updated_hosts = self._locked_hosts 127*9c5db199SXin Li if hosts: 128*9c5db199SXin Li unknown_hosts = set(hosts).difference(self._locked_hosts) 129*9c5db199SXin Li logging.warning('Skip unknown hosts: %s', unknown_hosts) 130*9c5db199SXin Li updated_hosts = set(hosts) - unknown_hosts 131*9c5db199SXin Li logging.info('Valid hosts: %s', updated_hosts) 132*9c5db199SXin Li updated_hosts = updated_hosts.intersection(self._locked_hosts) 133*9c5db199SXin Li 134*9c5db199SXin Li if not updated_hosts: 135*9c5db199SXin Li return False 136*9c5db199SXin Li 137*9c5db199SXin Li logging.info('Unlocking hosts (APs / PCAPs): %s', updated_hosts) 138*9c5db199SXin Li return self._host_modifier(updated_hosts, self.UNLOCK) 139*9c5db199SXin Li 140*9c5db199SXin Li 141*9c5db199SXin Li def _host_modifier(self, hosts, operation, lock_reason=None): 142*9c5db199SXin Li """Helper that locks hosts in DataStore. 143*9c5db199SXin Li 144*9c5db199SXin Li @param: hosts, a set of strings, host names. 145*9c5db199SXin Li @param operation: a string, LOCK or UNLOCK. 146*9c5db199SXin Li @param lock_reason: a string, a reason must be provided when locking. 147*9c5db199SXin Li 148*9c5db199SXin Li @returns a boolean, if operation succeeded on at least one host in 149*9c5db199SXin Li hosts. 150*9c5db199SXin Li """ 151*9c5db199SXin Li updated_hosts = set() 152*9c5db199SXin Li for host in hosts: 153*9c5db199SXin Li verified_host = self._check_host(host, operation) 154*9c5db199SXin Li if verified_host is not None: 155*9c5db199SXin Li updated_hosts.add(verified_host) 156*9c5db199SXin Li 157*9c5db199SXin Li logging.info('host_modifier: updated_hosts = %s', updated_hosts) 158*9c5db199SXin Li if not updated_hosts: 159*9c5db199SXin Li logging.info('host_modifier: no host to update') 160*9c5db199SXin Li return False 161*9c5db199SXin Li 162*9c5db199SXin Li for host in updated_hosts: 163*9c5db199SXin Li if operation == self.LOCK: 164*9c5db199SXin Li if self.dutils.lock_device(host, lock_reason): 165*9c5db199SXin Li logging.info('Locked host in datastore: %s', host) 166*9c5db199SXin Li self._locked_hosts = self._locked_hosts.union([host]) 167*9c5db199SXin Li else: 168*9c5db199SXin Li logging.error('Unable to lock host: ', host) 169*9c5db199SXin Li 170*9c5db199SXin Li if operation == self.UNLOCK: 171*9c5db199SXin Li if self.dutils.unlock_device(host): 172*9c5db199SXin Li logging.info('Unlocked host in datastore: %s', host) 173*9c5db199SXin Li self._locked_hosts = self._locked_hosts.difference([host]) 174*9c5db199SXin Li else: 175*9c5db199SXin Li logging.error('Unable to un-lock host: %s', host) 176*9c5db199SXin Li 177*9c5db199SXin Li return True 178*9c5db199SXin Li 179*9c5db199SXin Li 180*9c5db199SXin Liclass HostsLockedBy(object): 181*9c5db199SXin Li """Context manager to make sure that a HostLockManager will always unlock 182*9c5db199SXin Li its machines. This protects against both exceptions and SIGTERM.""" 183*9c5db199SXin Li 184*9c5db199SXin Li def _make_handler(self): 185*9c5db199SXin Li def _chaining_signal_handler(signal_number, frame): 186*9c5db199SXin Li self._manager.unlock() 187*9c5db199SXin Li # self._old_handler can also be signal.SIG_{IGN,DFL} which are ints. 188*9c5db199SXin Li if callable(self._old_handler): 189*9c5db199SXin Li self._old_handler(signal_number, frame) 190*9c5db199SXin Li return _chaining_signal_handler 191*9c5db199SXin Li 192*9c5db199SXin Li 193*9c5db199SXin Li def __init__(self, manager): 194*9c5db199SXin Li """ 195*9c5db199SXin Li @param manager: The HostLockManager used to lock the hosts. 196*9c5db199SXin Li """ 197*9c5db199SXin Li self._manager = manager 198*9c5db199SXin Li self._old_handler = signal.SIG_DFL 199*9c5db199SXin Li 200*9c5db199SXin Li 201*9c5db199SXin Li def __enter__(self): 202*9c5db199SXin Li self._old_handler = signal.signal(signal.SIGTERM, self._make_handler()) 203*9c5db199SXin Li 204*9c5db199SXin Li 205*9c5db199SXin Li def __exit__(self, exntype, exnvalue, backtrace): 206*9c5db199SXin Li signal.signal(signal.SIGTERM, self._old_handler) 207*9c5db199SXin Li self._manager.unlock() 208