| # Copyright (c) 2012 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 logging |
| import signal |
| |
| import common |
| from autotest_lib.server.cros.dynamic_suite import frontend_wrappers |
| |
| """HostLockManager class, for the dynamic_suite module. |
| |
| A HostLockManager instance manages locking and unlocking a set of autotest DUTs. |
| A caller can lock or unlock one or more DUTs. If the caller fails to unlock() |
| locked hosts before the instance is destroyed, it will attempt to unlock() the |
| hosts automatically, but this is to be avoided. |
| |
| Sample usage: |
| manager = host_lock_manager.HostLockManager() |
| try: |
| manager.lock(['host1']) |
| # do things |
| finally: |
| manager.unlock() |
| """ |
| |
| class HostLockManager(object): |
| """ |
| @attribute _afe: an instance of AFE as defined in server/frontend.py. |
| @attribute _locked_hosts: a set of DUT hostnames. |
| @attribute LOCK: a string. |
| @attribute UNLOCK: a string. |
| """ |
| |
| LOCK = 'lock' |
| UNLOCK = 'unlock' |
| |
| |
| @property |
| def locked_hosts(self): |
| """@returns set of locked hosts.""" |
| return self._locked_hosts |
| |
| |
| @locked_hosts.setter |
| def locked_hosts(self, hosts): |
| """Sets value of locked_hosts. |
| |
| @param hosts: a set of strings. |
| """ |
| self._locked_hosts = hosts |
| |
| |
| def __init__(self, afe=None): |
| """ |
| Constructor |
| |
| @param afe: an instance of AFE as defined in server/frontend.py. |
| """ |
| self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30, |
| delay_sec=10, |
| debug=False) |
| # Keep track of hosts locked by this instance. |
| self._locked_hosts = set() |
| |
| |
| def __del__(self): |
| if self._locked_hosts: |
| logging.warning('Caller failed to unlock %r! Forcing unlock now.', |
| self._locked_hosts) |
| self.unlock() |
| |
| |
| def _check_host(self, host, operation): |
| """Checks host for desired operation. |
| |
| @param host: a string, hostname. |
| @param operation: a string, LOCK or UNLOCK. |
| @returns a string: host name, if desired operation can be performed on |
| host or None otherwise. |
| """ |
| mod_host = host.split('.')[0] |
| host_info = self._afe.get_hosts(hostname=mod_host) |
| if not host_info: |
| logging.warning('Skip unknown host %s.', host) |
| return None |
| |
| host_info = host_info[0] |
| if operation == self.LOCK and host_info.locked: |
| err = ('Contention detected: %s is locked by %s at %s.' % |
| (mod_host, host_info.locked_by, host_info.lock_time)) |
| logging.warning(err) |
| return None |
| elif operation == self.UNLOCK and not host_info.locked: |
| logging.info('%s not locked.', mod_host) |
| return None |
| |
| return mod_host |
| |
| |
| def lock(self, hosts): |
| """Attempt to lock hosts in AFE. |
| |
| @param hosts: a list of strings, host names. |
| @returns a boolean, True == at least one host from hosts is locked. |
| """ |
| # Filter out hosts that we may have already locked |
| new_hosts = set(hosts).difference(self._locked_hosts) |
| logging.info('Attempt to lock %s', new_hosts) |
| if not new_hosts: |
| return False |
| |
| return self._host_modifier(new_hosts, self.LOCK) |
| |
| |
| def unlock(self, hosts=None): |
| """Unlock hosts in AFE. |
| |
| @param hosts: a list of strings, host names. |
| @returns a boolean, True == at least one host from self._locked_hosts is |
| unlocked. |
| """ |
| # Filter out hosts that we did not lock |
| updated_hosts = self._locked_hosts |
| if hosts: |
| unknown_hosts = set(hosts).difference(self._locked_hosts) |
| logging.warning('Skip unknown hosts: %s', unknown_hosts) |
| updated_hosts = set(hosts) - unknown_hosts |
| logging.info('Valid hosts: %s', updated_hosts) |
| updated_hosts = updated_hosts.intersection(self._locked_hosts) |
| |
| if not updated_hosts: |
| return False |
| |
| logging.info('Unlocking hosts: %s', updated_hosts) |
| return self._host_modifier(updated_hosts, self.UNLOCK) |
| |
| |
| def _host_modifier(self, hosts, operation): |
| """Helper that runs the modify_hosts() RPC with specified args. |
| |
| @param: hosts, a set of strings, host names. |
| @param operation: a string, LOCK or UNLOCK. |
| @returns a boolean, if operation succeeded on at least one host in |
| hosts. |
| """ |
| updated_hosts = set() |
| for host in hosts: |
| mod_host = self._check_host(host, operation) |
| if mod_host is not None: |
| updated_hosts.add(mod_host) |
| |
| logging.info('host_modifier: updated_hosts = %s', updated_hosts) |
| if not updated_hosts: |
| logging.info('host_modifier: no host to update') |
| return False |
| |
| kwargs = {'locked': True if operation == self.LOCK else False} |
| self._afe.run('modify_hosts', |
| host_filter_data={'hostname__in': list(updated_hosts)}, |
| update_data=kwargs) |
| |
| if operation == self.LOCK: |
| self._locked_hosts = self._locked_hosts.union(updated_hosts) |
| elif operation == self.UNLOCK: |
| self._locked_hosts = self._locked_hosts.difference(updated_hosts) |
| return True |
| |
| |
| class HostsLockedBy(object): |
| """Context manager to make sure that a HostLockManager will always unlock |
| its machines. This protects against both exceptions and SIGTERM.""" |
| |
| def _make_handler(self): |
| def _chaining_signal_handler(signal_number, frame): |
| self._manager.unlock() |
| # self._old_handler can also be signal.SIG_{IGN,DFL} which are ints. |
| if callable(self._old_handler): |
| self._old_handler(signal_number, frame) |
| return _chaining_signal_handler |
| |
| |
| def __init__(self, manager): |
| """ |
| @param manager: The HostLockManager used to lock the hosts. |
| """ |
| self._manager = manager |
| self._old_handler = signal.SIG_DFL |
| |
| |
| def __enter__(self): |
| self._old_handler = signal.signal(signal.SIGTERM, self._make_handler()) |
| |
| |
| def __exit__(self, exntype, exnvalue, backtrace): |
| signal.signal(signal.SIGTERM, self._old_handler) |
| self._manager.unlock() |