xref: /aosp_15_r20/external/autotest/server/cros/host_lock_manager.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Liimport logging
7*9c5db199SXin Liimport signal
8*9c5db199SXin Lifrom . import common
9*9c5db199SXin Li
10*9c5db199SXin Lifrom autotest_lib.server import site_utils
11*9c5db199SXin Lifrom autotest_lib.server.cros.chaos_lib import chaos_datastore_utils
12*9c5db199SXin Li"""HostLockManager class, for the dynamic_suite module.
13*9c5db199SXin Li
14*9c5db199SXin LiA HostLockManager instance manages locking and unlocking a set of autotest DUTs.
15*9c5db199SXin LiA caller can lock or unlock one or more DUTs. If the caller fails to unlock()
16*9c5db199SXin Lilocked hosts before the instance is destroyed, it will attempt to unlock() the
17*9c5db199SXin Lihosts automatically, but this is to be avoided.
18*9c5db199SXin Li
19*9c5db199SXin LiSample usage:
20*9c5db199SXin Li  manager = host_lock_manager.HostLockManager()
21*9c5db199SXin Li  try:
22*9c5db199SXin Li      manager.lock(['host1'])
23*9c5db199SXin Li      # do things
24*9c5db199SXin Li  finally:
25*9c5db199SXin Li      manager.unlock()
26*9c5db199SXin Li"""
27*9c5db199SXin Li
28*9c5db199SXin Liclass HostLockManager(object):
29*9c5db199SXin Li    """
30*9c5db199SXin Li    @attribute _afe: an instance of AFE as defined in server/frontend.py.
31*9c5db199SXin Li    @attribute _locked_hosts: a set of DUT hostnames.
32*9c5db199SXin Li    @attribute LOCK: a string.
33*9c5db199SXin Li    @attribute UNLOCK: a string.
34*9c5db199SXin Li    """
35*9c5db199SXin Li
36*9c5db199SXin Li    LOCK = 'lock'
37*9c5db199SXin Li    UNLOCK = 'unlock'
38*9c5db199SXin Li
39*9c5db199SXin Li
40*9c5db199SXin Li    @property
41*9c5db199SXin Li    def locked_hosts(self):
42*9c5db199SXin Li        """@returns set of locked hosts."""
43*9c5db199SXin Li        return self._locked_hosts
44*9c5db199SXin Li
45*9c5db199SXin Li
46*9c5db199SXin Li    @locked_hosts.setter
47*9c5db199SXin Li    def locked_hosts(self, hosts):
48*9c5db199SXin Li        """Sets value of locked_hosts.
49*9c5db199SXin Li
50*9c5db199SXin Li        @param hosts: a set of strings.
51*9c5db199SXin Li        """
52*9c5db199SXin Li        self._locked_hosts = hosts
53*9c5db199SXin Li
54*9c5db199SXin Li
55*9c5db199SXin Li    def __init__(self, afe=None):
56*9c5db199SXin Li        """
57*9c5db199SXin Li        Constructor
58*9c5db199SXin Li        """
59*9c5db199SXin Li        self.dutils = chaos_datastore_utils.ChaosDataStoreUtils()
60*9c5db199SXin Li        # Keep track of hosts locked by this instance.
61*9c5db199SXin Li        self._locked_hosts = set()
62*9c5db199SXin Li
63*9c5db199SXin Li
64*9c5db199SXin Li    def __del__(self):
65*9c5db199SXin Li        if self._locked_hosts:
66*9c5db199SXin Li            logging.warning('Caller failed to unlock %r! Forcing unlock now.',
67*9c5db199SXin Li                            self._locked_hosts)
68*9c5db199SXin Li            self.unlock()
69*9c5db199SXin Li
70*9c5db199SXin Li
71*9c5db199SXin Li    def _check_host(self, host, operation):
72*9c5db199SXin Li        """Checks host for desired operation.
73*9c5db199SXin Li
74*9c5db199SXin Li        @param host: a string, hostname.
75*9c5db199SXin Li        @param operation: a string, LOCK or UNLOCK.
76*9c5db199SXin Li        @returns a string: host name, if desired operation can be performed on
77*9c5db199SXin Li                           host or None otherwise.
78*9c5db199SXin Li        """
79*9c5db199SXin Li        host_checked = host
80*9c5db199SXin Li        # Get host details from DataStore
81*9c5db199SXin Li        host_info = self.dutils.show_device(host)
82*9c5db199SXin Li
83*9c5db199SXin Li        if not host_info:
84*9c5db199SXin Li            logging.warning('Host (AP) details not found in DataStore')
85*9c5db199SXin Li            return None
86*9c5db199SXin Li
87*9c5db199SXin Li        if operation == self.LOCK and host_info['lock_status']:
88*9c5db199SXin Li            err = ('Contention detected: %s is locked by %s at %s.' %
89*9c5db199SXin Li                   (host, host_info['locked_by'],
90*9c5db199SXin Li                    host_info['lock_status_updated']))
91*9c5db199SXin Li            logging.error(err)
92*9c5db199SXin Li            return None
93*9c5db199SXin Li
94*9c5db199SXin Li        elif operation == self.UNLOCK and not host_info['lock_status']:
95*9c5db199SXin Li            logging.info('%s not locked.', host)
96*9c5db199SXin Li            return None
97*9c5db199SXin Li
98*9c5db199SXin Li        return host_checked
99*9c5db199SXin Li
100*9c5db199SXin Li
101*9c5db199SXin Li    def lock(self, hosts, lock_reason='Locked by HostLockManager'):
102*9c5db199SXin Li        """Lock hosts in datastore.
103*9c5db199SXin Li
104*9c5db199SXin Li        @param hosts: a list of strings, host names.
105*9c5db199SXin Li        @param lock_reason: a string, a reason for locking the hosts.
106*9c5db199SXin Li
107*9c5db199SXin Li        @returns a boolean, True == at least one host from hosts is locked.
108*9c5db199SXin Li        """
109*9c5db199SXin Li        # Filter out hosts that we may have already locked
110*9c5db199SXin Li        new_hosts = set(hosts).difference(self._locked_hosts)
111*9c5db199SXin Li        logging.info('Attempt to lock %s', new_hosts)
112*9c5db199SXin Li        if not new_hosts:
113*9c5db199SXin Li            return False
114*9c5db199SXin Li
115*9c5db199SXin Li        return self._host_modifier(new_hosts, self.LOCK, lock_reason=lock_reason)
116*9c5db199SXin Li
117*9c5db199SXin Li
118*9c5db199SXin Li    def unlock(self, hosts=None):
119*9c5db199SXin Li        """Unlock hosts in datastore after use.
120*9c5db199SXin Li
121*9c5db199SXin Li        @param hosts: a list of strings, host names.
122*9c5db199SXin Li        @returns a boolean, True == at least one host from self._locked_hosts is
123*9c5db199SXin Li                 unlocked.
124*9c5db199SXin Li        """
125*9c5db199SXin Li        # Filter out hosts that we did not lock
126*9c5db199SXin Li        updated_hosts = self._locked_hosts
127*9c5db199SXin Li        if hosts:
128*9c5db199SXin Li            unknown_hosts = set(hosts).difference(self._locked_hosts)
129*9c5db199SXin Li            logging.warning('Skip unknown hosts: %s', unknown_hosts)
130*9c5db199SXin Li            updated_hosts = set(hosts) - unknown_hosts
131*9c5db199SXin Li            logging.info('Valid hosts: %s', updated_hosts)
132*9c5db199SXin Li            updated_hosts = updated_hosts.intersection(self._locked_hosts)
133*9c5db199SXin Li
134*9c5db199SXin Li        if not updated_hosts:
135*9c5db199SXin Li            return False
136*9c5db199SXin Li
137*9c5db199SXin Li        logging.info('Unlocking hosts (APs / PCAPs): %s', updated_hosts)
138*9c5db199SXin Li        return self._host_modifier(updated_hosts, self.UNLOCK)
139*9c5db199SXin Li
140*9c5db199SXin Li
141*9c5db199SXin Li    def _host_modifier(self, hosts, operation, lock_reason=None):
142*9c5db199SXin Li        """Helper that locks hosts in DataStore.
143*9c5db199SXin Li
144*9c5db199SXin Li        @param: hosts, a set of strings, host names.
145*9c5db199SXin Li        @param operation: a string, LOCK or UNLOCK.
146*9c5db199SXin Li        @param lock_reason: a string, a reason must be provided when locking.
147*9c5db199SXin Li
148*9c5db199SXin Li        @returns a boolean, if operation succeeded on at least one host in
149*9c5db199SXin Li                 hosts.
150*9c5db199SXin Li        """
151*9c5db199SXin Li        updated_hosts = set()
152*9c5db199SXin Li        for host in hosts:
153*9c5db199SXin Li            verified_host = self._check_host(host, operation)
154*9c5db199SXin Li            if verified_host is not None:
155*9c5db199SXin Li                updated_hosts.add(verified_host)
156*9c5db199SXin Li
157*9c5db199SXin Li        logging.info('host_modifier: updated_hosts = %s', updated_hosts)
158*9c5db199SXin Li        if not updated_hosts:
159*9c5db199SXin Li            logging.info('host_modifier: no host to update')
160*9c5db199SXin Li            return False
161*9c5db199SXin Li
162*9c5db199SXin Li        for host in updated_hosts:
163*9c5db199SXin Li            if operation == self.LOCK:
164*9c5db199SXin Li                if self.dutils.lock_device(host, lock_reason):
165*9c5db199SXin Li                    logging.info('Locked host in datastore: %s', host)
166*9c5db199SXin Li                    self._locked_hosts = self._locked_hosts.union([host])
167*9c5db199SXin Li                else:
168*9c5db199SXin Li                    logging.error('Unable to lock host: ', host)
169*9c5db199SXin Li
170*9c5db199SXin Li            if operation == self.UNLOCK:
171*9c5db199SXin Li                if self.dutils.unlock_device(host):
172*9c5db199SXin Li                    logging.info('Unlocked host in datastore: %s', host)
173*9c5db199SXin Li                    self._locked_hosts = self._locked_hosts.difference([host])
174*9c5db199SXin Li                else:
175*9c5db199SXin Li                    logging.error('Unable to un-lock host: %s', host)
176*9c5db199SXin Li
177*9c5db199SXin Li        return True
178*9c5db199SXin Li
179*9c5db199SXin Li
180*9c5db199SXin Liclass HostsLockedBy(object):
181*9c5db199SXin Li    """Context manager to make sure that a HostLockManager will always unlock
182*9c5db199SXin Li    its machines. This protects against both exceptions and SIGTERM."""
183*9c5db199SXin Li
184*9c5db199SXin Li    def _make_handler(self):
185*9c5db199SXin Li        def _chaining_signal_handler(signal_number, frame):
186*9c5db199SXin Li            self._manager.unlock()
187*9c5db199SXin Li            # self._old_handler can also be signal.SIG_{IGN,DFL} which are ints.
188*9c5db199SXin Li            if callable(self._old_handler):
189*9c5db199SXin Li                self._old_handler(signal_number, frame)
190*9c5db199SXin Li        return _chaining_signal_handler
191*9c5db199SXin Li
192*9c5db199SXin Li
193*9c5db199SXin Li    def __init__(self, manager):
194*9c5db199SXin Li        """
195*9c5db199SXin Li        @param manager: The HostLockManager used to lock the hosts.
196*9c5db199SXin Li        """
197*9c5db199SXin Li        self._manager = manager
198*9c5db199SXin Li        self._old_handler = signal.SIG_DFL
199*9c5db199SXin Li
200*9c5db199SXin Li
201*9c5db199SXin Li    def __enter__(self):
202*9c5db199SXin Li        self._old_handler = signal.signal(signal.SIGTERM, self._make_handler())
203*9c5db199SXin Li
204*9c5db199SXin Li
205*9c5db199SXin Li    def __exit__(self, exntype, exnvalue, backtrace):
206*9c5db199SXin Li        signal.signal(signal.SIGTERM, self._old_handler)
207*9c5db199SXin Li        self._manager.unlock()
208