blob: e2f54a62d7dcf180e13719600242f29982900b38 [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
101 def lock(self, hosts):
102 """Attempt to lock hosts in AFE.
103
104 @param hosts: a list of strings, host names.
105 @returns a boolean, True == at least one host from hosts is locked.
106 """
107 # Filter out hosts that we may have already locked
108 new_hosts = set(hosts).difference(self._locked_hosts)
109 logging.info('Attempt to lock %s', new_hosts)
110 if not new_hosts:
Tan Gaoe6f9c092013-05-02 17:16:12 -0700111 return False
112
Tan Gao751b8542013-05-13 17:24:17 -0700113 return self._host_modifier(new_hosts, self.LOCK)
Chris Masone9807bd62012-07-11 14:44:17 -0700114
115
Tan Gao751b8542013-05-13 17:24:17 -0700116 def unlock(self, hosts=None):
117 """Unlock hosts in AFE.
Chris Masone9807bd62012-07-11 14:44:17 -0700118
Tan Gao751b8542013-05-13 17:24:17 -0700119 @param hosts: a list of strings, host names.
120 @returns a boolean, True == at least one host from self._locked_hosts is
121 unlocked.
Chris Masone9807bd62012-07-11 14:44:17 -0700122 """
Tan Gao751b8542013-05-13 17:24:17 -0700123 # Filter out hosts that we did not lock
124 updated_hosts = self._locked_hosts
125 if hosts:
126 unknown_hosts = set(hosts).difference(self._locked_hosts)
127 logging.warning('Skip unknown hosts: %s', unknown_hosts)
128 updated_hosts = set(hosts) - unknown_hosts
129 logging.info('Valid hosts: %s', updated_hosts)
130 updated_hosts = updated_hosts.intersection(self._locked_hosts)
131
132 if not updated_hosts:
133 return False
134
135 logging.info('Unlocking hosts: %s', updated_hosts)
136 return self._host_modifier(updated_hosts, self.UNLOCK)
137
138
139 def _host_modifier(self, hosts, operation):
140 """Helper that runs the modify_hosts() RPC with specified args.
141
142 @param: hosts, a set of strings, host names.
143 @param operation: a string, LOCK or UNLOCK.
144 @returns a boolean, if operation succeeded on at least one host in
145 hosts.
146 """
147 updated_hosts = set()
148 for host in hosts:
149 mod_host = self._check_host(host, operation)
150 if mod_host is not None:
151 updated_hosts.add(mod_host)
152
153 logging.info('host_modifier: updated_hosts = %s', updated_hosts)
154 if not updated_hosts:
155 logging.info('host_modifier: no host to update')
156 return False
157
158 kwargs = {'locked': True if operation == self.LOCK else False}
Chris Masone9807bd62012-07-11 14:44:17 -0700159 self._afe.run('modify_hosts',
Tan Gao751b8542013-05-13 17:24:17 -0700160 host_filter_data={'hostname__in': list(updated_hosts)},
Chris Masone9807bd62012-07-11 14:44:17 -0700161 update_data=kwargs)
Alex Miller8dca4642012-08-30 16:08:32 -0700162
Tan Gao751b8542013-05-13 17:24:17 -0700163 if operation == self.LOCK:
164 self._locked_hosts = self._locked_hosts.union(updated_hosts)
165 elif operation == self.UNLOCK:
166 self._locked_hosts = self._locked_hosts.difference(updated_hosts)
167 return True
168
Alex Miller8dca4642012-08-30 16:08:32 -0700169
170class HostsLockedBy(object):
171 """Context manager to make sure that a HostLockManager will always unlock
172 its machines. This protects against both exceptions and SIGTERM."""
173
174 def _make_handler(self):
175 def _chaining_signal_handler(signal_number, frame):
176 self._manager.unlock()
177 # self._old_handler can also be signal.SIG_{IGN,DFL} which are ints.
178 if callable(self._old_handler):
179 self._old_handler(signal_number, frame)
180 return _chaining_signal_handler
181
182
183 def __init__(self, manager):
184 """
185 @param manager: The HostLockManager used to lock the hosts.
186 """
187 self._manager = manager
188 self._old_handler = signal.SIG_DFL
189
190
191 def __enter__(self):
192 self._old_handler = signal.signal(signal.SIGTERM, self._make_handler())
193
194
195 def __exit__(self, exntype, exnvalue, backtrace):
196 signal.signal(signal.SIGTERM, self._old_handler)
197 self._manager.unlock()