blob: 0cdded1cff3c83ad7204693f198423cb3123ea66 [file] [log] [blame]
Chris Masone9807bd62012-07-11 14:44:17 -07001# 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
Alex Miller8dca4642012-08-30 16:08:32 -07006import signal
Chris Masone9807bd62012-07-11 14:44:17 -07007
8import common
Chris Masoneb4935552012-08-14 12:05:54 -07009from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Chris Masone9807bd62012-07-11 14:44:17 -070010
11"""HostLockManager class, for the dynamic_suite module.
12
Tan Gao751b8542013-05-13 17:24:17 -070013A HostLockManager instance manages locking and unlocking a set of autotest DUTs.
14A caller can lock or unlock one or more DUTs. If the caller fails to unlock()
15locked hosts before the instance is destroyed, it will attempt to unlock() the
16hosts automatically, but this is to be avoided.
Chris Masone9807bd62012-07-11 14:44:17 -070017
Tan Gao751b8542013-05-13 17:24:17 -070018Sample usage:
Chris Masone9807bd62012-07-11 14:44:17 -070019 manager = host_lock_manager.HostLockManager()
20 try:
Tan Gao751b8542013-05-13 17:24:17 -070021 manager.lock(['host1'])
Chris Masone9807bd62012-07-11 14:44:17 -070022 # do things
23 finally:
24 manager.unlock()
25"""
26
27class HostLockManager(object):
28 """
Tan Gao751b8542013-05-13 17:24:17 -070029 @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 Masone9807bd62012-07-11 14:44:17 -070033 """
34
Tan Gao751b8542013-05-13 17:24:17 -070035 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 Masone9807bd62012-07-11 14:44:17 -070053
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 Gao751b8542013-05-13 17:24:17 -070063 # Keep track of hosts locked by this instance.
64 self._locked_hosts = set()
Chris Masone9807bd62012-07-11 14:44:17 -070065
66
67 def __del__(self):
Tan Gao751b8542013-05-13 17:24:17 -070068 if self._locked_hosts:
69 logging.warning('Caller failed to unlock %r! Forcing unlock now.',
70 self._locked_hosts)
Chris Masone9807bd62012-07-11 14:44:17 -070071 self.unlock()
72
73
Tan Gao751b8542013-05-13 17:24:17 -070074 def _check_host(self, host, operation):
75 """Checks host for desired operation.
Tan Gaoe6f9c092013-05-02 17:16:12 -070076
77 @param host: a string, hostname.
Tan Gao751b8542013-05-13 17:24:17 -070078 @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 Gaoe6f9c092013-05-02 17:16:12 -070081 """
82 mod_host = host.split('.')[0]
Tan Gao5f6de122013-05-07 13:05:53 -070083 host_info = self._afe.get_hosts(hostname=mod_host)
84 if not host_info:
Tan Gao751b8542013-05-13 17:24:17 -070085 logging.warning('Skip unknown host %s.', host)
86 return None
Tan Gao5f6de122013-05-07 13:05:53 -070087
88 host_info = host_info[0]
Tan Gao751b8542013-05-13 17:24:17 -070089 if operation == self.LOCK and host_info.locked:
Tan Gaoe6f9c092013-05-02 17:16:12 -070090 err = ('Contention detected: %s is locked by %s at %s.' %
Tan Gao5f6de122013-05-07 13:05:53 -070091 (mod_host, host_info.locked_by, host_info.lock_time))
Tan Gao751b8542013-05-13 17:24:17 -070092 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 Sartori394c2be2015-05-12 11:28:26 -0700101 def lock(self, hosts, lock_reason='Locked by HostLockManager'):
Tan Gao751b8542013-05-13 17:24:17 -0700102 """Attempt to lock hosts in AFE.
103
104 @param hosts: a list of strings, host names.
Matthew Sartori394c2be2015-05-12 11:28:26 -0700105 @param lock_reason: a string, a reason for locking the hosts.
106
Tan Gao751b8542013-05-13 17:24:17 -0700107 @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 Gaoe6f9c092013-05-02 17:16:12 -0700113 return False
114
Matthew Sartori394c2be2015-05-12 11:28:26 -0700115 return self._host_modifier(new_hosts, self.LOCK, lock_reason=lock_reason)
Chris Masone9807bd62012-07-11 14:44:17 -0700116
117
Tan Gao751b8542013-05-13 17:24:17 -0700118 def unlock(self, hosts=None):
119 """Unlock hosts in AFE.
Chris Masone9807bd62012-07-11 14:44:17 -0700120
Tan Gao751b8542013-05-13 17:24:17 -0700121 @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 Masone9807bd62012-07-11 14:44:17 -0700124 """
Tan Gao751b8542013-05-13 17:24:17 -0700125 # 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 Sartori394c2be2015-05-12 11:28:26 -0700141 def _host_modifier(self, hosts, operation, lock_reason=None):
Tan Gao751b8542013-05-13 17:24:17 -0700142 """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 Sartori394c2be2015-05-12 11:28:26 -0700146 @param lock_reason: a string, a reason must be provided when locking.
147
Tan Gao751b8542013-05-13 17:24:17 -0700148 @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 Sartori394c2be2015-05-12 11:28:26 -0700163 if operation == self.LOCK:
164 kwargs['lock_reason'] = lock_reason
Chris Masone9807bd62012-07-11 14:44:17 -0700165 self._afe.run('modify_hosts',
Tan Gao751b8542013-05-13 17:24:17 -0700166 host_filter_data={'hostname__in': list(updated_hosts)},
Chris Masone9807bd62012-07-11 14:44:17 -0700167 update_data=kwargs)
Alex Miller8dca4642012-08-30 16:08:32 -0700168
Matthew Sartori394c2be2015-05-12 11:28:26 -0700169 if operation == self.LOCK and lock_reason:
Tan Gao751b8542013-05-13 17:24:17 -0700170 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 Miller8dca4642012-08-30 16:08:32 -0700175
176class 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()