Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 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 | |
| 5 | import logging |
Alex Miller | 8dca464 | 2012-08-30 16:08:32 -0700 | [diff] [blame] | 6 | import signal |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 7 | |
| 8 | import common |
Chris Masone | b493555 | 2012-08-14 12:05:54 -0700 | [diff] [blame] | 9 | from autotest_lib.server.cros.dynamic_suite import frontend_wrappers |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 10 | |
| 11 | """HostLockManager class, for the dynamic_suite module. |
| 12 | |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 13 | A HostLockManager instance manages locking and unlocking a set of autotest DUTs. |
| 14 | A caller can lock or unlock one or more DUTs. If the caller fails to unlock() |
| 15 | locked hosts before the instance is destroyed, it will attempt to unlock() the |
| 16 | hosts automatically, but this is to be avoided. |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 17 | |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 18 | Sample usage: |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 19 | manager = host_lock_manager.HostLockManager() |
| 20 | try: |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 21 | manager.lock(['host1']) |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 22 | # do things |
| 23 | finally: |
| 24 | manager.unlock() |
| 25 | """ |
| 26 | |
| 27 | class HostLockManager(object): |
| 28 | """ |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 29 | @attribute _afe: an instance of AFE as defined in server/frontend.py. |
| 30 | @attribute _locked_hosts: a set of DUT hostnames. |
| 31 | @attribute LOCK: a string. |
| 32 | @attribute UNLOCK: a string. |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 33 | """ |
| 34 | |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 35 | LOCK = 'lock' |
| 36 | UNLOCK = 'unlock' |
| 37 | |
| 38 | |
| 39 | @property |
| 40 | def locked_hosts(self): |
| 41 | """@returns set of locked hosts.""" |
| 42 | return self._locked_hosts |
| 43 | |
| 44 | |
| 45 | @locked_hosts.setter |
| 46 | def locked_hosts(self, hosts): |
| 47 | """Sets value of locked_hosts. |
| 48 | |
| 49 | @param hosts: a set of strings. |
| 50 | """ |
| 51 | self._locked_hosts = hosts |
| 52 | |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 53 | |
| 54 | def __init__(self, afe=None): |
| 55 | """ |
| 56 | Constructor |
| 57 | |
| 58 | @param afe: an instance of AFE as defined in server/frontend.py. |
| 59 | """ |
| 60 | self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30, |
| 61 | delay_sec=10, |
| 62 | debug=False) |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 63 | # Keep track of hosts locked by this instance. |
| 64 | self._locked_hosts = set() |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 65 | |
| 66 | |
| 67 | def __del__(self): |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 68 | if self._locked_hosts: |
| 69 | logging.warning('Caller failed to unlock %r! Forcing unlock now.', |
| 70 | self._locked_hosts) |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 71 | self.unlock() |
| 72 | |
| 73 | |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 74 | def _check_host(self, host, operation): |
| 75 | """Checks host for desired operation. |
Tan Gao | e6f9c09 | 2013-05-02 17:16:12 -0700 | [diff] [blame] | 76 | |
| 77 | @param host: a string, hostname. |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 78 | @param operation: a string, LOCK or UNLOCK. |
| 79 | @returns a string: host name, if desired operation can be performed on |
| 80 | host or None otherwise. |
Tan Gao | e6f9c09 | 2013-05-02 17:16:12 -0700 | [diff] [blame] | 81 | """ |
| 82 | mod_host = host.split('.')[0] |
Tan Gao | 5f6de12 | 2013-05-07 13:05:53 -0700 | [diff] [blame] | 83 | host_info = self._afe.get_hosts(hostname=mod_host) |
| 84 | if not host_info: |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 85 | logging.warning('Skip unknown host %s.', host) |
| 86 | return None |
Tan Gao | 5f6de12 | 2013-05-07 13:05:53 -0700 | [diff] [blame] | 87 | |
| 88 | host_info = host_info[0] |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 89 | if operation == self.LOCK and host_info.locked: |
Tan Gao | e6f9c09 | 2013-05-02 17:16:12 -0700 | [diff] [blame] | 90 | err = ('Contention detected: %s is locked by %s at %s.' % |
Tan Gao | 5f6de12 | 2013-05-07 13:05:53 -0700 | [diff] [blame] | 91 | (mod_host, host_info.locked_by, host_info.lock_time)) |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 92 | logging.warning(err) |
| 93 | return None |
| 94 | elif operation == self.UNLOCK and not host_info.locked: |
| 95 | logging.info('%s not locked.', mod_host) |
| 96 | return None |
| 97 | |
| 98 | return mod_host |
| 99 | |
| 100 | |
Matthew Sartori | 394c2be | 2015-05-12 11:28:26 -0700 | [diff] [blame^] | 101 | def lock(self, hosts, lock_reason='Locked by HostLockManager'): |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 102 | """Attempt to lock hosts in AFE. |
| 103 | |
| 104 | @param hosts: a list of strings, host names. |
Matthew Sartori | 394c2be | 2015-05-12 11:28:26 -0700 | [diff] [blame^] | 105 | @param lock_reason: a string, a reason for locking the hosts. |
| 106 | |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 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: |
Tan Gao | e6f9c09 | 2013-05-02 17:16:12 -0700 | [diff] [blame] | 113 | return False |
| 114 | |
Matthew Sartori | 394c2be | 2015-05-12 11:28:26 -0700 | [diff] [blame^] | 115 | return self._host_modifier(new_hosts, self.LOCK, lock_reason=lock_reason) |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 116 | |
| 117 | |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 118 | def unlock(self, hosts=None): |
| 119 | """Unlock hosts in AFE. |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 120 | |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 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. |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 124 | """ |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 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: %s', updated_hosts) |
| 138 | return self._host_modifier(updated_hosts, self.UNLOCK) |
| 139 | |
| 140 | |
Matthew Sartori | 394c2be | 2015-05-12 11:28:26 -0700 | [diff] [blame^] | 141 | def _host_modifier(self, hosts, operation, lock_reason=None): |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 142 | """Helper that runs the modify_hosts() RPC with specified args. |
| 143 | |
| 144 | @param: hosts, a set of strings, host names. |
| 145 | @param operation: a string, LOCK or UNLOCK. |
Matthew Sartori | 394c2be | 2015-05-12 11:28:26 -0700 | [diff] [blame^] | 146 | @param lock_reason: a string, a reason must be provided when locking. |
| 147 | |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 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 | mod_host = self._check_host(host, operation) |
| 154 | if mod_host is not None: |
| 155 | updated_hosts.add(mod_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 | kwargs = {'locked': True if operation == self.LOCK else False} |
Matthew Sartori | 394c2be | 2015-05-12 11:28:26 -0700 | [diff] [blame^] | 163 | if operation == self.LOCK: |
| 164 | kwargs['lock_reason'] = lock_reason |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 165 | self._afe.run('modify_hosts', |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 166 | host_filter_data={'hostname__in': list(updated_hosts)}, |
Chris Masone | 9807bd6 | 2012-07-11 14:44:17 -0700 | [diff] [blame] | 167 | update_data=kwargs) |
Alex Miller | 8dca464 | 2012-08-30 16:08:32 -0700 | [diff] [blame] | 168 | |
Matthew Sartori | 394c2be | 2015-05-12 11:28:26 -0700 | [diff] [blame^] | 169 | if operation == self.LOCK and lock_reason: |
Tan Gao | 751b854 | 2013-05-13 17:24:17 -0700 | [diff] [blame] | 170 | self._locked_hosts = self._locked_hosts.union(updated_hosts) |
| 171 | elif operation == self.UNLOCK: |
| 172 | self._locked_hosts = self._locked_hosts.difference(updated_hosts) |
| 173 | return True |
| 174 | |
Alex Miller | 8dca464 | 2012-08-30 16:08:32 -0700 | [diff] [blame] | 175 | |
| 176 | class HostsLockedBy(object): |
| 177 | """Context manager to make sure that a HostLockManager will always unlock |
| 178 | its machines. This protects against both exceptions and SIGTERM.""" |
| 179 | |
| 180 | def _make_handler(self): |
| 181 | def _chaining_signal_handler(signal_number, frame): |
| 182 | self._manager.unlock() |
| 183 | # self._old_handler can also be signal.SIG_{IGN,DFL} which are ints. |
| 184 | if callable(self._old_handler): |
| 185 | self._old_handler(signal_number, frame) |
| 186 | return _chaining_signal_handler |
| 187 | |
| 188 | |
| 189 | def __init__(self, manager): |
| 190 | """ |
| 191 | @param manager: The HostLockManager used to lock the hosts. |
| 192 | """ |
| 193 | self._manager = manager |
| 194 | self._old_handler = signal.SIG_DFL |
| 195 | |
| 196 | |
| 197 | def __enter__(self): |
| 198 | self._old_handler = signal.signal(signal.SIGTERM, self._make_handler()) |
| 199 | |
| 200 | |
| 201 | def __exit__(self, exntype, exnvalue, backtrace): |
| 202 | signal.signal(signal.SIGTERM, self._old_handler) |
| 203 | self._manager.unlock() |