1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import logging 6import signal 7import common 8 9from autotest_lib.server import site_utils 10from autotest_lib.server.cros.dynamic_suite import frontend_wrappers 11 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 @param afe: an instance of AFE as defined in server/frontend.py. 60 """ 61 self._afe = afe or frontend_wrappers.RetryingAFE( 62 timeout_min=30, delay_sec=10, debug=False, 63 server=site_utils.get_global_afe_hostname()) 64 # Keep track of hosts locked by this instance. 65 self._locked_hosts = set() 66 67 68 def __del__(self): 69 if self._locked_hosts: 70 logging.warning('Caller failed to unlock %r! Forcing unlock now.', 71 self._locked_hosts) 72 self.unlock() 73 74 75 def _check_host(self, host, operation): 76 """Checks host for desired operation. 77 78 @param host: a string, hostname. 79 @param operation: a string, LOCK or UNLOCK. 80 @returns a string: host name, if desired operation can be performed on 81 host or None otherwise. 82 """ 83 mod_host = host.split('.')[0] 84 host_info = self._afe.get_hosts(hostname=mod_host) 85 if not host_info: 86 logging.warning('Skip unknown host %s.', host) 87 return None 88 89 host_info = host_info[0] 90 if operation == self.LOCK and host_info.locked: 91 err = ('Contention detected: %s is locked by %s at %s.' % 92 (mod_host, host_info.locked_by, host_info.lock_time)) 93 logging.warning(err) 94 return None 95 elif operation == self.UNLOCK and not host_info.locked: 96 logging.info('%s not locked.', mod_host) 97 return None 98 99 return mod_host 100 101 102 def lock(self, hosts, lock_reason='Locked by HostLockManager'): 103 """Attempt to lock hosts in AFE. 104 105 @param hosts: a list of strings, host names. 106 @param lock_reason: a string, a reason for locking the hosts. 107 108 @returns a boolean, True == at least one host from hosts is locked. 109 """ 110 # Filter out hosts that we may have already locked 111 new_hosts = set(hosts).difference(self._locked_hosts) 112 logging.info('Attempt to lock %s', new_hosts) 113 if not new_hosts: 114 return False 115 116 return self._host_modifier(new_hosts, self.LOCK, lock_reason=lock_reason) 117 118 119 def unlock(self, hosts=None): 120 """Unlock hosts in AFE. 121 122 @param hosts: a list of strings, host names. 123 @returns a boolean, True == at least one host from self._locked_hosts is 124 unlocked. 125 """ 126 # Filter out hosts that we did not lock 127 updated_hosts = self._locked_hosts 128 if hosts: 129 unknown_hosts = set(hosts).difference(self._locked_hosts) 130 logging.warning('Skip unknown hosts: %s', unknown_hosts) 131 updated_hosts = set(hosts) - unknown_hosts 132 logging.info('Valid hosts: %s', updated_hosts) 133 updated_hosts = updated_hosts.intersection(self._locked_hosts) 134 135 if not updated_hosts: 136 return False 137 138 logging.info('Unlocking hosts: %s', updated_hosts) 139 return self._host_modifier(updated_hosts, self.UNLOCK) 140 141 142 def _host_modifier(self, hosts, operation, lock_reason=None): 143 """Helper that runs the modify_hosts() RPC with specified args. 144 145 @param: hosts, a set of strings, host names. 146 @param operation: a string, LOCK or UNLOCK. 147 @param lock_reason: a string, a reason must be provided when locking. 148 149 @returns a boolean, if operation succeeded on at least one host in 150 hosts. 151 """ 152 updated_hosts = set() 153 for host in hosts: 154 mod_host = self._check_host(host, operation) 155 if mod_host is not None: 156 updated_hosts.add(mod_host) 157 158 logging.info('host_modifier: updated_hosts = %s', updated_hosts) 159 if not updated_hosts: 160 logging.info('host_modifier: no host to update') 161 return False 162 163 kwargs = {'locked': True if operation == self.LOCK else False} 164 if operation == self.LOCK: 165 kwargs['lock_reason'] = lock_reason 166 self._afe.run('modify_hosts', 167 host_filter_data={'hostname__in': list(updated_hosts)}, 168 update_data=kwargs) 169 170 if operation == self.LOCK and lock_reason: 171 self._locked_hosts = self._locked_hosts.union(updated_hosts) 172 elif operation == self.UNLOCK: 173 self._locked_hosts = self._locked_hosts.difference(updated_hosts) 174 return True 175 176 177class HostsLockedBy(object): 178 """Context manager to make sure that a HostLockManager will always unlock 179 its machines. This protects against both exceptions and SIGTERM.""" 180 181 def _make_handler(self): 182 def _chaining_signal_handler(signal_number, frame): 183 self._manager.unlock() 184 # self._old_handler can also be signal.SIG_{IGN,DFL} which are ints. 185 if callable(self._old_handler): 186 self._old_handler(signal_number, frame) 187 return _chaining_signal_handler 188 189 190 def __init__(self, manager): 191 """ 192 @param manager: The HostLockManager used to lock the hosts. 193 """ 194 self._manager = manager 195 self._old_handler = signal.SIG_DFL 196 197 198 def __enter__(self): 199 self._old_handler = signal.signal(signal.SIGTERM, self._make_handler()) 200 201 202 def __exit__(self, exntype, exnvalue, backtrace): 203 signal.signal(signal.SIGTERM, self._old_handler) 204 self._manager.unlock() 205