xref: /aosp_15_r20/external/autotest/server/cros/host_lock_manager.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import logging
7import signal
8from . import common
9
10from autotest_lib.server import site_utils
11from autotest_lib.server.cros.chaos_lib import chaos_datastore_utils
12"""HostLockManager class, for the dynamic_suite module.
13
14A 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.
18
19Sample usage:
20  manager = host_lock_manager.HostLockManager()
21  try:
22      manager.lock(['host1'])
23      # do things
24  finally:
25      manager.unlock()
26"""
27
28class HostLockManager(object):
29    """
30    @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.
34    """
35
36    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
54
55    def __init__(self, afe=None):
56        """
57        Constructor
58        """
59        self.dutils = chaos_datastore_utils.ChaosDataStoreUtils()
60        # Keep track of hosts locked by this instance.
61        self._locked_hosts = set()
62
63
64    def __del__(self):
65        if self._locked_hosts:
66            logging.warning('Caller failed to unlock %r! Forcing unlock now.',
67                            self._locked_hosts)
68            self.unlock()
69
70
71    def _check_host(self, host, operation):
72        """Checks host for desired operation.
73
74        @param host: a string, hostname.
75        @param operation: a string, LOCK or UNLOCK.
76        @returns a string: host name, if desired operation can be performed on
77                           host or None otherwise.
78        """
79        host_checked = host
80        # Get host details from DataStore
81        host_info = self.dutils.show_device(host)
82
83        if not host_info:
84            logging.warning('Host (AP) details not found in DataStore')
85            return None
86
87        if operation == self.LOCK and host_info['lock_status']:
88            err = ('Contention detected: %s is locked by %s at %s.' %
89                   (host, host_info['locked_by'],
90                    host_info['lock_status_updated']))
91            logging.error(err)
92            return None
93
94        elif operation == self.UNLOCK and not host_info['lock_status']:
95            logging.info('%s not locked.', host)
96            return None
97
98        return host_checked
99
100
101    def lock(self, hosts, lock_reason='Locked by HostLockManager'):
102        """Lock hosts in datastore.
103
104        @param hosts: a list of strings, host names.
105        @param lock_reason: a string, a reason for locking the hosts.
106
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:
113            return False
114
115        return self._host_modifier(new_hosts, self.LOCK, lock_reason=lock_reason)
116
117
118    def unlock(self, hosts=None):
119        """Unlock hosts in datastore after use.
120
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.
124        """
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 (APs / PCAPs): %s', updated_hosts)
138        return self._host_modifier(updated_hosts, self.UNLOCK)
139
140
141    def _host_modifier(self, hosts, operation, lock_reason=None):
142        """Helper that locks hosts in DataStore.
143
144        @param: hosts, a set of strings, host names.
145        @param operation: a string, LOCK or UNLOCK.
146        @param lock_reason: a string, a reason must be provided when locking.
147
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            verified_host = self._check_host(host, operation)
154            if verified_host is not None:
155                updated_hosts.add(verified_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        for host in updated_hosts:
163            if operation == self.LOCK:
164                if self.dutils.lock_device(host, lock_reason):
165                    logging.info('Locked host in datastore: %s', host)
166                    self._locked_hosts = self._locked_hosts.union([host])
167                else:
168                    logging.error('Unable to lock host: ', host)
169
170            if operation == self.UNLOCK:
171                if self.dutils.unlock_device(host):
172                    logging.info('Unlocked host in datastore: %s', host)
173                    self._locked_hosts = self._locked_hosts.difference([host])
174                else:
175                    logging.error('Unable to un-lock host: %s', host)
176
177        return True
178
179
180class HostsLockedBy(object):
181    """Context manager to make sure that a HostLockManager will always unlock
182    its machines. This protects against both exceptions and SIGTERM."""
183
184    def _make_handler(self):
185        def _chaining_signal_handler(signal_number, frame):
186            self._manager.unlock()
187            # self._old_handler can also be signal.SIG_{IGN,DFL} which are ints.
188            if callable(self._old_handler):
189                self._old_handler(signal_number, frame)
190        return _chaining_signal_handler
191
192
193    def __init__(self, manager):
194        """
195        @param manager: The HostLockManager used to lock the hosts.
196        """
197        self._manager = manager
198        self._old_handler = signal.SIG_DFL
199
200
201    def __enter__(self):
202        self._old_handler = signal.signal(signal.SIGTERM, self._make_handler())
203
204
205    def __exit__(self, exntype, exnvalue, backtrace):
206        signal.signal(signal.SIGTERM, self._old_handler)
207        self._manager.unlock()
208