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