blob: 2c88c20a8fd7847e7b818da41b6699167f7805e4 [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 -07007import common
Tien Changa96ac8f2015-12-04 10:28:49 -08008
9from autotest_lib.server import site_utils
Chris Masoneb4935552012-08-14 12:05:54 -070010from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Chris Masone9807bd62012-07-11 14:44:17 -070011
12"""HostLockManager class, for the dynamic_suite module.
13
Tan Gao751b8542013-05-13 17:24:17 -070014A 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.
Chris Masone9807bd62012-07-11 14:44:17 -070018
Tan Gao751b8542013-05-13 17:24:17 -070019Sample usage:
Chris Masone9807bd62012-07-11 14:44:17 -070020 manager = host_lock_manager.HostLockManager()
21 try:
Tan Gao751b8542013-05-13 17:24:17 -070022 manager.lock(['host1'])
Chris Masone9807bd62012-07-11 14:44:17 -070023 # do things
24 finally:
25 manager.unlock()
26"""
27
28class HostLockManager(object):
29 """
Tan Gao751b8542013-05-13 17:24:17 -070030 @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.
Chris Masone9807bd62012-07-11 14:44:17 -070034 """
35
Tan Gao751b8542013-05-13 17:24:17 -070036 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
Chris Masone9807bd62012-07-11 14:44:17 -070054
55 def __init__(self, afe=None):
56 """
57 Constructor
58
59 @param afe: an instance of AFE as defined in server/frontend.py.
60 """
Tien Changa96ac8f2015-12-04 10:28:49 -080061 self._afe = afe or frontend_wrappers.RetryingAFE(
62 timeout_min=30, delay_sec=10, debug=False,
63 server=site_utils.get_global_afe_hostname())
Tan Gao751b8542013-05-13 17:24:17 -070064 # Keep track of hosts locked by this instance.
65 self._locked_hosts = set()
Chris Masone9807bd62012-07-11 14:44:17 -070066
67
68 def __del__(self):
Tan Gao751b8542013-05-13 17:24:17 -070069 if self._locked_hosts:
70 logging.warning('Caller failed to unlock %r! Forcing unlock now.',
71 self._locked_hosts)
Chris Masone9807bd62012-07-11 14:44:17 -070072 self.unlock()
73
74
Tan Gao751b8542013-05-13 17:24:17 -070075 def _check_host(self, host, operation):
76 """Checks host for desired operation.
Tan Gaoe6f9c092013-05-02 17:16:12 -070077
78 @param host: a string, hostname.
Tan Gao751b8542013-05-13 17:24:17 -070079 @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.
Tan Gaoe6f9c092013-05-02 17:16:12 -070082 """
83 mod_host = host.split('.')[0]
Tan Gao5f6de122013-05-07 13:05:53 -070084 host_info = self._afe.get_hosts(hostname=mod_host)
85 if not host_info:
Tan Gao751b8542013-05-13 17:24:17 -070086 logging.warning('Skip unknown host %s.', host)
87 return None
Tan Gao5f6de122013-05-07 13:05:53 -070088
89 host_info = host_info[0]
Tan Gao751b8542013-05-13 17:24:17 -070090 if operation == self.LOCK and host_info.locked:
Tan Gaoe6f9c092013-05-02 17:16:12 -070091 err = ('Contention detected: %s is locked by %s at %s.' %
Tan Gao5f6de122013-05-07 13:05:53 -070092 (mod_host, host_info.locked_by, host_info.lock_time))
Tan Gao751b8542013-05-13 17:24:17 -070093 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
Matthew Sartori394c2be2015-05-12 11:28:26 -0700102 def lock(self, hosts, lock_reason='Locked by HostLockManager'):
Tan Gao751b8542013-05-13 17:24:17 -0700103 """Attempt to lock hosts in AFE.
104
105 @param hosts: a list of strings, host names.
Matthew Sartori394c2be2015-05-12 11:28:26 -0700106 @param lock_reason: a string, a reason for locking the hosts.
107
Tan Gao751b8542013-05-13 17:24:17 -0700108 @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:
Tan Gaoe6f9c092013-05-02 17:16:12 -0700114 return False
115
Matthew Sartori394c2be2015-05-12 11:28:26 -0700116 return self._host_modifier(new_hosts, self.LOCK, lock_reason=lock_reason)
Chris Masone9807bd62012-07-11 14:44:17 -0700117
118
Tan Gao751b8542013-05-13 17:24:17 -0700119 def unlock(self, hosts=None):
120 """Unlock hosts in AFE.
Chris Masone9807bd62012-07-11 14:44:17 -0700121
Tan Gao751b8542013-05-13 17:24:17 -0700122 @param hosts: a list of strings, host names.
123 @returns a boolean, True == at least one host from self._locked_hosts is
124 unlocked.
Chris Masone9807bd62012-07-11 14:44:17 -0700125 """
Tan Gao751b8542013-05-13 17:24:17 -0700126 # 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
Matthew Sartori394c2be2015-05-12 11:28:26 -0700142 def _host_modifier(self, hosts, operation, lock_reason=None):
Tan Gao751b8542013-05-13 17:24:17 -0700143 """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.
Matthew Sartori394c2be2015-05-12 11:28:26 -0700147 @param lock_reason: a string, a reason must be provided when locking.
148
Tan Gao751b8542013-05-13 17:24:17 -0700149 @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}
Matthew Sartori394c2be2015-05-12 11:28:26 -0700164 if operation == self.LOCK:
165 kwargs['lock_reason'] = lock_reason
Chris Masone9807bd62012-07-11 14:44:17 -0700166 self._afe.run('modify_hosts',
Tan Gao751b8542013-05-13 17:24:17 -0700167 host_filter_data={'hostname__in': list(updated_hosts)},
Chris Masone9807bd62012-07-11 14:44:17 -0700168 update_data=kwargs)
Alex Miller8dca4642012-08-30 16:08:32 -0700169
Matthew Sartori394c2be2015-05-12 11:28:26 -0700170 if operation == self.LOCK and lock_reason:
Tan Gao751b8542013-05-13 17:24:17 -0700171 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
Alex Miller8dca4642012-08-30 16:08:32 -0700176
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()