1# Copyright (c) 2013 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 collections 6import httplib 7import logging 8import os 9import re 10import socket 11import time 12 13import common 14 15from autotest_lib.client.common_lib import error 16from autotest_lib.client.common_lib.cros import retry 17from autotest_lib.server import utils 18 19 20GOOFY_JSONRPC_SERVER_PORT = 0x0FAC 21GOOFY_RUNNING = 'RUNNING' 22 23 24class GoofyProxyException(Exception): 25 """Exception raised when a goofy rpc fails.""" 26 pass 27 28 29class GoofyRuntimeException(Exception): 30 """Exception raised when something goes wrong while a test is running.""" 31 pass 32 33 34def retry_goofy_rpc(exception_tuple, timeout_min=30): 35 """A decorator to use with goofy rpcs. 36 37 This decorator tries to recreate the goofy client proxy on 38 socket error. It will continue trying to do so until it 39 executes the method without any socket errors or till the 40 retry.retry decorator hits it's timeout. 41 42 Usage: 43 If you just want to recreate the proxy: 44 1. @retry_goofy_rpc(exception_tuple=(<exception>, socket.error), 45 timeout_min=<timeout>) 46 2. @retry_goofy_rpc(socket.error, timeout_min=<timeout>) 47 Note: you need to specify the socket.error exception because we 48 want to retry the call after recreating the proxy. 49 50 @param exception_tuple: A tuple of exceptions to pass to 51 the retry decorator. Any of these exceptions will result 52 in retries for the duration of timeout_min. 53 @param timeout_min: The timeout, in minutes, for which we should 54 retry the method ignoring any exception in exception_tuple. 55 """ 56 def inner_decorator(method): 57 """Inner retry decorator applied to the method. 58 59 @param method: The method that needs to be wrapped in the decorator. 60 61 @return A wrapper function that implements the retry. 62 """ 63 64 @retry.retry(exception_tuple, timeout_min=timeout_min) 65 def wrapper(*args, **kwargs): 66 """This wrapper handles socket errors. 67 68 If the method in question: 69 1. Throws an exception in exception_tuple and it is not a 70 socket.error, retry for timeout_min through retry.retry. 71 2. Throws a socket.error, recreate the client proxy, and 72 retry for timeout_min through retry.retry. 73 3. Throws an exception not in exception_tuple, fail. 74 """ 75 try: 76 return method(*args, **kwargs) 77 except socket.error as e: 78 goofy_proxy = args[0] 79 if type(goofy_proxy) is GoofyProxy: 80 logging.warning('Socket error while running factory tests ' 81 '%s, recreating goofy proxy.', e) 82 goofy_proxy._create_client_proxy(timeout_min=timeout_min) 83 else: 84 logging.warning('Connectivity was lost and the retry ' 85 'decorator was unable to recreate a goofy ' 86 'client proxy, args: %s.', args) 87 raise 88 89 return wrapper 90 91 return inner_decorator 92 93 94class GoofyProxy(object): 95 """Client capable of making rpc calls to goofy. 96 97 Methods of this class that can cause goofy to change state 98 usually need a retry decorator. Methods that have a retry decorator 99 need to be 'pure', i.e return the same results when called multiple 100 times with the same argument. 101 102 There are 2 known exceptions this class can deal with, a socket.error 103 which happens when we try to execute an rpc when the DUT is, say, suspended 104 and a BadStatusLine, which we get when we try to execute an rpc while the 105 DUT is going through a factory_restart. Ideally we would like to handle 106 socket timeouts different from BadStatusLines as we can get connection 107 errors even when a device reboots and BadStatusLines ususally only when 108 factory restarts. crbug.com/281714. 109 """ 110 111 # This timeout was arbitrarily chosen as many tests in the factory test 112 # suite run for days. Ideally we would like to split this into at least 2 113 # timeouts, one which we use for rpcs that run while no other test is, 114 # running and is smaller than the second that is designed for use with rpcs 115 # that might execute simultaneously with a test. The latter needs a longer 116 # timeout since tests could suspend,resume for a long time, and a call like 117 # GetGoofyStatus should be tolerant to these suspend/resumes. In designing 118 # the base timeout one needs to allocate time to component methods of this 119 # class (such as _set_test_list) as a multiple of the number of rpcs it 120 # executes. 121 BASE_RPC_TIMEOUT = 1440 122 POLLING_INTERVAL = 5 123 FACTORY_BUG_RE = r'.*(/tmp/factory_bug.*tar.bz2).*' 124 UNTAR_COMMAND = 'tar jxf %s -C %s' 125 126 127 def __init__(self, host): 128 """ 129 @param host: The host object representing the DUT running goofy. 130 """ 131 self._host = host 132 self._raw_stop_running_tests() 133 self._create_client_proxy(timeout_min=self.BASE_RPC_TIMEOUT) 134 135 136 def _create_client_proxy(self, timeout_min=30): 137 """Create a goofy client proxy. 138 139 Ping the host till it's up, then proceed to create a goofy proxy. We 140 don't wrap this method with a retry because it's used in the retry 141 decorator itself. 142 """ 143 144 # We don't ssh ping here as there is a potential dealy in O(minutes) 145 # with our ssh command against a sleeping DUT, once it wakes up, and 146 # that will lead to significant overhead incurred over many reboots. 147 self._host.ping_wait_up(timeout_min) 148 logging.info('Host is pingable, creating goofy client proxy') 149 self._client = self._host.rpc_server_tracker.jsonrpc_connect( 150 GOOFY_JSONRPC_SERVER_PORT) 151 152 153 @retry.retry((httplib.BadStatusLine, socket.error), 154 timeout_min=BASE_RPC_TIMEOUT) 155 def _raw_stop_running_tests(self): 156 """Stop running tests by shelling out to the DUT. 157 158 Use this method only before we have actually created the client 159 proxy, as shelling out has several pitfalls. We need to stop all 160 tests in a retry loop because tests will start executing as soon 161 as we have reimaged a DUT and trying to create the proxy while 162 the DUT is rebooting will lead to a spurious failure. 163 164 Note that we use the plain retry decorator for this method since 165 we don't need to recreate the client proxy on failure. 166 """ 167 logging.info('Stopping all tests and clearing factory state') 168 self._host.run('factory clear') 169 170 171 @retry_goofy_rpc((httplib.BadStatusLine, socket.error), 172 timeout_min=BASE_RPC_TIMEOUT) 173 def _get_goofy_status(self): 174 """Return status of goofy, ignoring socket timeouts and http exceptions. 175 """ 176 status = self._client.GetGoofyStatus().get('status') 177 return status 178 179 180 def _wait_for_goofy(self, timeout_min=BASE_RPC_TIMEOUT*2): 181 """Wait till goofy is running or a timeout occurs. 182 183 @param timeout_min: Minutes to wait before timing this call out. 184 """ 185 current_time = time.time() 186 timeout_secs = timeout_min * 60 187 logging.info('Waiting on goofy') 188 while self._get_goofy_status() != GOOFY_RUNNING: 189 if time.time() - current_time > timeout_secs: 190 break 191 return 192 193 194 @retry_goofy_rpc(socket.error, timeout_min=BASE_RPC_TIMEOUT*2) 195 def _set_test_list(self, next_list): 196 """Set the given test list for execution and turn on test automation. 197 198 Confirm that the given test list is a test that has been baked into 199 the image, then run it. Some test lists are configured to start 200 execution automatically when we call SetTestList, while others wait 201 for a corresponding RunTest. 202 203 @param next_list: The name of the test list. 204 205 @raise jsonrpclib.ProtocolError: If the test list we're trying to switch 206 to isn't on the DUT. 207 """ 208 209 # We can get a BadStatus line on 2 occassions: 210 # 1. As part of SwitchTestList goofy performs a factory restart, which 211 # will throw a BadStatusLine because the rpc can't exit cleanly. We 212 # don't want to retry on this exception, since we've already set the 213 # right test list. 214 # 2. If we try to set a test list while goofy is already down 215 # (from a previous factory restart). In this case we wouldn't have 216 # set the new test list, because we coulnd't connect to goofy. 217 # To properly set a new test list it's important to wait till goofy is 218 # up before attempting to set the test list, while being aware that the 219 # preceding httplib error is from the rpc we just executed leading to 220 # a factory restart. Also note that if the test list is not already on 221 # the DUT this method will fail, emitting the possible test lists one 222 # can switch to. 223 self._wait_for_goofy() 224 logging.info('Switching to test list %s', next_list) 225 try: 226 # Enable full factory test automation. Full test automation mode 227 # skips all manual tests and test barriers, which is what we want in 228 # the test lab. There are other automation modes: partial and none. 229 # In partial automation mode manual tests and barrier are enabled 230 # and user intervention is required; none disables automation. 231 self._client.SwitchTestList(next_list, 'full') 232 except httplib.BadStatusLine: 233 logging.info('Switched to list %s, goofy restarting', next_list) 234 235 236 @retry_goofy_rpc((httplib.BadStatusLine, socket.error), 237 timeout_min=BASE_RPC_TIMEOUT*2) 238 def _stop_running_tests(self): 239 """Stop all running tests. 240 241 Wrap the StopTest rpc so we can attempt to stop tests even while a DUT 242 is suspended or rebooting. 243 """ 244 logging.info('Stopping tests.') 245 self._client.StopTest() 246 247 248 def _get_test_map(self): 249 """Get a mapping of test suites -> tests. 250 251 Ignore entries for tests that don't have a path. 252 253 @return: A dictionary of the form 254 {'suite_name': ['suite_name.path_to_test', ...]}. 255 """ 256 test_all = set([test['path'] for test in self._client.GetTests() 257 if test.get('path')]) 258 259 test_map = collections.defaultdict(list) 260 for names in test_all: 261 test_map[names.split('.')[0]].append(names) 262 return test_map 263 264 265 def _log_test_results(self, test_status, current_suite): 266 """Format test status results and write them to status.log. 267 268 @param test_status: The status dictionary of a single test. 269 @param current_suite: The current suite name. 270 """ 271 try: 272 self._host.job.record('INFO', None, None, 273 'suite %s, test %s, status: %s' % 274 (current_suite, test_status.get('path'), 275 test_status.get('status'))) 276 except AttributeError as e: 277 logging.error('Could not gather results for current test: %s', e) 278 279 280 @retry_goofy_rpc((httplib.BadStatusLine, socket.error), 281 timeout_min=BASE_RPC_TIMEOUT*2) 282 def _get_test_info(self, test_name): 283 """Get the status of one test. 284 285 @param test_name: The name of the test we need the status of. 286 287 @return: The entry for the test in the status dictionary. 288 """ 289 for test in self._client.GetTests(): 290 if test['path'] == test_name: 291 return test 292 raise ValueError('Could not find test_name %s in _get_test_info.' % 293 test_name) 294 295 296 @retry_goofy_rpc((httplib.BadStatusLine, socket.error), 297 timeout_min=BASE_RPC_TIMEOUT*2) 298 def _get_test_run_info(self, run_id): 299 """Get the information about the given test run. 300 301 @param run_id: The ID of the test run. 302 303 @return: A dict of test run status. 304 """ 305 return self._client.GetTestRunStatus(run_id) 306 307 308 def _wait_on_run(self, run_id): 309 """Wait until the given test run to end. 310 311 @param run_id: The ID of the test run. 312 313 @raises GoofyRuntimeException: If the test run does not finish 314 gracefully. 315 """ 316 finished_tests = set() 317 run_info = self._get_test_run_info(run_id) 318 while run_info['status'] == 'RUNNING': 319 finished = [(t['path'], t['status']) for t in 320 run_info['scheduled_tests'] 321 if t['status'] in ('PASSED', 'FAILED')] 322 for t in finished: 323 if t not in finished_tests: 324 logging.info('[%s] %s', t[1], t[0]) 325 finished_tests.add(t) 326 time.sleep(self.POLLING_INTERVAL) 327 run_info = self._get_test_run_info(run_id) 328 if run_info['status'] != 'FINISHED': 329 raise GoofyRuntimeException( 330 'The requested test run was interrupted.') 331 332 333 def _synchronous_run_suite(self, suite_name): 334 """Run one suite and wait for it to finish. 335 336 Will start a test run for the specified suite_name and wait until it 337 ends. 338 339 @param suite_name: The name of the suite to wait for. 340 341 @raises GoofyProxyException: If the status of the suite 342 doesn't switch to active after we call RunTest. 343 344 @return: The result of the suite. 345 """ 346 logging.info('Starting suite: %s', suite_name) 347 run_id = self._client.RunTest(suite_name) 348 self._wait_on_run(run_id) 349 return self._get_test_run_info(run_id) 350 351 352 def monitor_tests(self, test_list): 353 """Run a test list. 354 355 Will run each suite in the given list in sequence, starting each one 356 by name and waiting on its results. This method makes the following 357 assumptions: 358 - A test list is made up of self contained suites. 359 - These suites trigger several things in parallel. 360 - After a suite finishes it leaves goofy in an idle state. 361 362 It is not safe to pull results for individual tests during the suite 363 as the device could be rebooting, or goofy could be under stress. 364 Instead, this method synchronously waits on an entire suite, then 365 asks goofy for the status of each test in the suite. Since certain 366 test lists automatically start and others don't, this method stops 367 test list execution regardless, and sequentially triggers each suite. 368 369 @param test_list: The test list to run. 370 """ 371 self._set_test_list(test_list) 372 self._wait_for_goofy() 373 self._stop_running_tests() 374 375 test_map = self._get_test_map() 376 if test_map: 377 logging.info('About to execute tests: %s', test_map) 378 else: 379 raise GoofyRuntimeException('Test map is empty, you might have an ' 380 'error in your test_list.') 381 382 383 for current_suite in test_map.keys(): 384 logging.info('Processing suite %s', current_suite) 385 386 result = self._synchronous_run_suite(current_suite) 387 logging.info(result) 388 389 for test_names in test_map.get(current_suite): 390 self._log_test_results(self._get_test_info(test_names), 391 current_suite) 392 393 394 @retry_goofy_rpc((httplib.BadStatusLine, socket.error), 395 timeout_min=BASE_RPC_TIMEOUT*2) 396 def get_results(self, resultsdir): 397 """Copies results from the DUT to a local results directory. 398 399 Copy the tarball over to the results folder, untar, and delete the 400 tarball if everything was successful. This will effectively place 401 all the logs relevant to factory testing in the job's results folder. 402 403 @param resultsdir: The directory in which to untar the contents of the 404 tarball factory_bug generates. 405 """ 406 logging.info('Getting results logs for test_list.') 407 408 try: 409 factory_bug_log = self._host.run('factory_bug').stderr 410 except error.CmdError as e: 411 logging.error('Could not execute factory_bug: %s', e) 412 return 413 414 try: 415 factory_bug_tar = re.match(self.FACTORY_BUG_RE, 416 factory_bug_log).groups(1)[0] 417 except (IndexError, AttributeError): 418 logging.error('could not collect logs for factory results, ' 419 'factory bug returned %s', factory_bug_log) 420 return 421 422 factory_bug_tar_file = os.path.basename(factory_bug_tar) 423 local_factory_bug_tar = os.path.join(resultsdir, factory_bug_tar_file) 424 425 try: 426 self._host.get_file(factory_bug_tar, local_factory_bug_tar) 427 except error.AutoservRunError as e: 428 logging.error('Failed to pull back the results tarball: %s', e) 429 return 430 431 try: 432 utils.run(self.UNTAR_COMMAND % (local_factory_bug_tar, resultsdir)) 433 except error.CmdError as e: 434 logging.error('Failed to untar the results tarball: %s', e) 435 return 436 finally: 437 if os.path.exists(local_factory_bug_tar): 438 os.remove(local_factory_bug_tar) 439