• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2013 The Chromium 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
6import random
7import requests
8
9from time import sleep
10
11import common
12from autotest_lib.client.common_lib import utils
13from autotest_lib.server.cros.ap_configurators import \
14    ap_configurator_factory
15from autotest_lib.client.common_lib.cros.network import ap_constants
16from autotest_lib.server.cros.ap_configurators import ap_cartridge
17
18
19# Max number of retry attempts to lock an ap.
20MAX_RETRIES = 3
21CHAOS_URL = 'https://chaos-188802.appspot.com'
22
23
24class ApLocker(object):
25    """Object to keep track of AP lock state.
26
27    @attribute configurator: an APConfigurator object.
28    @attribute to_be_locked: a boolean, True iff ap has not been locked.
29    @attribute retries: an integer, max number of retry attempts to lock ap.
30    """
31
32
33    def __init__(self, configurator, retries):
34        """Initialize.
35
36        @param configurator: an APConfigurator object.
37        @param retries: an integer, max number of retry attempts to lock ap.
38        """
39        self.configurator = configurator
40        self.to_be_locked = True
41        self.retries = retries
42
43
44    def __repr__(self):
45        """@return class name, ap host name, lock status and retries."""
46        return 'class: %s, host name: %s, to_be_locked = %s, retries = %d' % (
47                self.__class__.__name__,
48                self.configurator.host_name,
49                self.to_be_locked,
50                self.retries)
51
52
53def construct_ap_lockers(ap_spec, retries, hostname_matching_only=False,
54                         ap_test_type=ap_constants.AP_TEST_TYPE_CHAOS):
55    """Convert APConfigurator objects to ApLocker objects for locking.
56
57    @param ap_spec: an APSpec object
58    @param retries: an integer, max number of retry attempts to lock ap.
59    @param hostname_matching_only: a boolean, if True matching against
60                                   all other APSpec parameters is not
61                                   performed.
62    @param ap_test_type: Used to determine which type of test we're
63                         currently running (Chaos vs Clique).
64
65    @return a list of ApLocker objects.
66    """
67    ap_lockers_list = []
68    factory = ap_configurator_factory.APConfiguratorFactory(ap_test_type,
69                                                            ap_spec)
70    if hostname_matching_only:
71        for ap in factory.get_aps_by_hostnames(ap_spec.hostnames):
72            ap_lockers_list.append(ApLocker(ap, retries))
73    else:
74        for ap in factory.get_ap_configurators_by_spec(ap_spec):
75            ap_lockers_list.append(ApLocker(ap, retries))
76
77    if not len(ap_lockers_list):
78        logging.error('Found no matching APs to test against for %s', ap_spec)
79
80    logging.debug('Found %d APs', len(ap_lockers_list))
81    return ap_lockers_list
82
83
84class ApBatchLocker(object):
85    """Object to lock/unlock an APConfigurator.
86
87    @attribute SECONDS_TO_SLEEP: an integer, number of seconds to sleep between
88                                 retries.
89    @attribute ap_spec: an APSpec object
90    @attribute retries: an integer, max number of retry attempts to lock ap.
91                        Defaults to MAX_RETRIES.
92    @attribute aps_to_lock: a list of ApLocker objects.
93    @attribute manager: a HostLockManager object, used to lock/unlock APs.
94    """
95
96
97    MIN_SECONDS_TO_SLEEP = 30
98    MAX_SECONDS_TO_SLEEP = 120
99
100
101    def __init__(self, lock_manager, ap_spec, retries=MAX_RETRIES,
102                 hostname_matching_only=False,
103                 ap_test_type=ap_constants.AP_TEST_TYPE_CHAOS):
104        """Initialize.
105
106        @param ap_spec: an APSpec object
107        @param retries: an integer, max number of retry attempts to lock ap.
108                        Defaults to MAX_RETRIES.
109        @param hostname_matching_only : a boolean, if True matching against
110                                        all other APSpec parameters is not
111                                        performed.
112        @param ap_test_type: Used to determine which type of test we're
113                             currently running (Chaos vs Clique).
114        """
115        self.aps_to_lock = construct_ap_lockers(ap_spec, retries,
116                           hostname_matching_only=hostname_matching_only,
117                           ap_test_type=ap_test_type)
118        self.manager = lock_manager
119        self._locked_aps = []
120
121
122    def has_more_aps(self):
123        """@return True iff there is at least one AP to be locked."""
124        return len(self.aps_to_lock) > 0
125
126
127    def lock_ap_in_afe(self, ap_locker):
128        """Locks an AP host in AFE.
129
130        @param ap_locker: an ApLocker object, AP to be locked.
131        @return a boolean, True iff ap_locker is locked.
132        """
133        if not utils.host_is_in_lab_zone(ap_locker.configurator.host_name):
134            ap_locker.to_be_locked = False
135            return True
136
137        if self.manager.lock([ap_locker.configurator.host_name]):
138            self._locked_aps.append(ap_locker)
139            logging.info('locked %s', ap_locker.configurator.host_name)
140            ap_locker.to_be_locked = False
141            return True
142        else:
143            ap_locker.retries -= 1
144            logging.info('%d retries left for %s',
145                         ap_locker.retries,
146                         ap_locker.configurator.host_name)
147            if ap_locker.retries == 0:
148                logging.info('No more retries left. Remove %s from list',
149                             ap_locker.configurator.host_name)
150                ap_locker.to_be_locked = False
151
152        return False
153
154    def lock_ap_in_datastore(self, ap_locker):
155        """Locks an AP host in datastore.
156
157        @param ap_locker: an ApLocker object, AP to be locked.
158        @return a boolean, True iff ap_locker is locked.
159        """
160        if not utils.host_is_in_lab_zone(ap_locker.configurator.host_name):
161            ap_locker.to_be_locked = False
162            return True
163
164        # Begin locking device in datastore.
165        locked_device = requests.put(CHAOS_URL + '/devices/lock', \
166                        json={"hostname":[ap_locker.configurator.host_name], \
167                        "locked_by":"TestRun"})
168        if locked_device.json()['result']:
169            self._locked_aps.append(ap_locker)
170            logging.info('locked %s', ap_locker.configurator.host_name)
171            ap_locker.to_be_locked = False
172            return True
173        else:
174            ap_locker.retries -= 1
175            logging.info('%d retries left for %s',
176                         ap_locker.retries,
177                         ap_locker.configurator.host_name)
178            if ap_locker.retries == 0:
179                logging.info('No more retries left. Remove %s from list',
180                             ap_locker.configurator.host_name)
181                ap_locker.to_be_locked = False
182
183        return False
184
185
186    def get_ap_batch(self, batch_size=ap_cartridge.THREAD_MAX):
187        """Allocates a batch of locked APs.
188
189        @param batch_size: an integer, max. number of aps to lock in one batch.
190                           Defaults to THREAD_MAX in ap_cartridge.py
191        @return a list of APConfigurator objects, locked on AFE.
192        """
193        # We need this while loop to continuously loop over the for loop.
194        # To exit the while loop, we either:
195        #  - locked batch_size number of aps and return them
196        #  - exhausted all retries on all aps in aps_to_lock
197        while len(self.aps_to_lock):
198            ap_batch = []
199
200            for ap_locker in self.aps_to_lock:
201                logging.info('checking %s', ap_locker.configurator.host_name)
202                # TODO(@rjahagir): Change method to datastore.
203                # if self.lock_ap_in_datastore(ap_locker):
204                if self.lock_ap_in_afe(ap_locker):
205                    ap_batch.append(ap_locker.configurator)
206                    if len(ap_batch) == batch_size:
207                        break
208
209            # Remove locked APs from list of APs to process.
210            aps_to_rm = [ap for ap in self.aps_to_lock if not ap.to_be_locked]
211            self.aps_to_lock = list(set(self.aps_to_lock) - set(aps_to_rm))
212            for ap in aps_to_rm:
213                logging.info('Removed %s from self.aps_to_lock',
214                             ap.configurator.host_name)
215            logging.info('Remaining aps to lock = %s',
216                         [ap.configurator.host_name for ap in self.aps_to_lock])
217
218            # Return available APs and retry remaining ones later.
219            if ap_batch:
220                return ap_batch
221
222            # Sleep before next retry.
223            if self.aps_to_lock:
224                seconds_to_sleep = random.randint(self.MIN_SECONDS_TO_SLEEP,
225                                                  self.MAX_SECONDS_TO_SLEEP)
226                logging.info('Sleep %d sec before retry', seconds_to_sleep)
227                sleep(seconds_to_sleep)
228
229        return []
230
231
232    def unlock_one_ap(self, host_name):
233        """Unlock one AP after we're done.
234
235        @param host_name: a string, host name.
236        """
237        for ap_locker in self._locked_aps:
238            if host_name == ap_locker.configurator.host_name:
239                self.manager.unlock(hosts=[host_name])
240                self._locked_aps.remove(ap_locker)
241                return
242
243        logging.error('Tried to unlock a host we have not locked (%s)?',
244                      host_name)
245
246    def unlock_one_ap_in_datastore(self, host_name):
247        """Unlock one AP from datastore after we're done.
248
249        @param host_name: a string, host name.
250        """
251        for ap_locker in self._locked_aps:
252            if host_name == ap_locker.configurator.host_name:
253                # Unlock in datastore
254                unlocked_device = requests.put(CHAOS_URL + '/devices/unlock', \
255                                  json={"hostname":host_name})
256                # TODO: Raise error if unable to unlock.
257                if not unlocked_device.json()['result']:
258                    raise error
259                    logging.debug(unlocked_device.content())
260                else:
261                    self._locked_aps.remove(ap_locker)
262                return
263
264        logging.error('Tried to unlock a host we have not locked (%s)?',
265                      host_name)
266
267
268    def unlock_aps(self):
269        """Unlock APs after we're done."""
270        # Make a copy of all of the hostnames to process
271        host_names = list()
272        for ap_locker in self._locked_aps:
273            host_names.append(ap_locker.configurator.host_name)
274        for host_name in host_names:
275            # TODO(@rjahagir): Change method to datastore.
276            # self.unlock_one_ap_in_datastore(host_name)
277            self.unlock_one_ap(host_name)
278
279
280    def unlock_and_reclaim_ap(self, host_name):
281        """Unlock an AP but return it to the remaining batch of APs.
282
283        @param host_name: a string, host name.
284        """
285        for ap_locker in self._locked_aps:
286            if host_name == ap_locker.configurator.host_name:
287                self.aps_to_lock.append(ap_locker)
288                # TODO(@rjahagir): Change method to datastore.
289                # self.unlock_one_ap_in_datastore(host_name)
290                self.unlock_one_ap(host_name)
291                return
292
293
294    def unlock_and_reclaim_aps(self):
295        """Unlock APs but return them to the batch of remining APs.
296
297        unlock_aps() will remove the remaining APs from the list of all APs
298        to process.  This method will add the remaining APs back to the pool
299        of unprocessed APs.
300
301        """
302        # Add the APs back into the pool
303        self.aps_to_lock.extend(self._locked_aps)
304        self.unlock_aps()
305