blob: 2bdf8b08fcacd151e8980f65dcea40f3715866df [file] [log] [blame]
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001#!/usr/bin/env python
2#
3# Copyright 2016 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
Fang Deng26e4dc12018-03-04 19:01:59 -080016"""Common Utilities."""
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070017
Sam Chiu81bdc652018-06-29 18:45:08 +080018from __future__ import print_function
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070019import base64
20import binascii
21import errno
Fang Deng69498c32017-03-02 14:29:30 -080022import getpass
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070023import logging
24import os
25import shutil
26import struct
Fang Deng69498c32017-03-02 14:29:30 -080027import subprocess
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070028import sys
29import tarfile
30import tempfile
31import time
32import uuid
33
herbertxue34776bb2018-07-03 21:57:48 +080034from acloud.internal import constants
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070035from acloud.public import errors
36
37logger = logging.getLogger(__name__)
38
Fang Deng69498c32017-03-02 14:29:30 -080039SSH_KEYGEN_CMD = ["ssh-keygen", "-t", "rsa", "-b", "4096"]
cylan4f73c1f2018-07-19 16:40:31 +080040SSH_KEYGEN_PUB_CMD = ["ssh-keygen", "-y"]
Kevin Chengd25feee2018-05-24 10:15:20 -070041DEFAULT_RETRY_BACKOFF_FACTOR = 1
42DEFAULT_SLEEP_MULTIPLIER = 0
Fang Deng69498c32017-03-02 14:29:30 -080043
44
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070045class TempDir(object):
Fang Deng26e4dc12018-03-04 19:01:59 -080046 """A context manager that ceates a temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070047
Fang Deng26e4dc12018-03-04 19:01:59 -080048 Attributes:
Sam Chiu81bdc652018-06-29 18:45:08 +080049 path: The path of the temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070050 """
51
Fang Deng26e4dc12018-03-04 19:01:59 -080052 def __init__(self):
53 self.path = tempfile.mkdtemp()
54 os.chmod(self.path, 0o700)
55 logger.debug("Created temporary dir %s", self.path)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070056
57 def __enter__(self):
Fang Deng26e4dc12018-03-04 19:01:59 -080058 """Enter."""
59 return self.path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070060
Fang Deng26e4dc12018-03-04 19:01:59 -080061 def __exit__(self, exc_type, exc_value, traceback):
62 """Exit.
63
64 Args:
65 exc_type: Exception type raised within the context manager.
66 None if no execption is raised.
67 exc_value: Exception instance raised within the context manager.
68 None if no execption is raised.
69 traceback: Traceback for exeception that is raised within
70 the context manager.
71 None if no execption is raised.
72 Raises:
73 EnvironmentError or OSError when failed to delete temp directory.
74 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070075 try:
Fang Deng26e4dc12018-03-04 19:01:59 -080076 if self.path:
77 shutil.rmtree(self.path)
78 logger.debug("Deleted temporary dir %s", self.path)
79 except EnvironmentError as e:
80 # Ignore error if there is no exception raised
81 # within the with-clause and the EnvironementError is
82 # about problem that directory or file does not exist.
83 if not exc_type and e.errno != errno.ENOENT:
84 raise
85 except Exception as e: # pylint: disable=W0703
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070086 if exc_type:
Fang Deng26e4dc12018-03-04 19:01:59 -080087 logger.error(
cylan0d77ae12018-05-18 08:36:48 +000088 "Encountered error while deleting %s: %s",
89 self.path,
90 str(e),
91 exc_info=True)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070092 else:
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070093 raise
94
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070095
cylan0d77ae12018-05-18 08:36:48 +000096def RetryOnException(retry_checker,
97 max_retries,
98 sleep_multiplier=0,
Fang Dengf24be082018-02-10 10:09:55 -080099 retry_backoff_factor=1):
cylan0d77ae12018-05-18 08:36:48 +0000100 """Decorater which retries the function call if |retry_checker| returns true.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700101
cylan0d77ae12018-05-18 08:36:48 +0000102 Args:
103 retry_checker: A callback function which should take an exception instance
104 and return True if functor(*args, **kwargs) should be retried
105 when such exception is raised, and return False if it should
106 not be retried.
107 max_retries: Maximum number of retries allowed.
108 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
109 retry_backoff_factor is 1. Will sleep
110 sleep_multiplier * (
111 retry_backoff_factor ** (attempt_count - 1))
112 if retry_backoff_factor != 1.
113 retry_backoff_factor: See explanation of sleep_multiplier.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700114
cylan0d77ae12018-05-18 08:36:48 +0000115 Returns:
116 The function wrapper.
117 """
118
119 def _Wrapper(func):
120 def _FunctionWrapper(*args, **kwargs):
121 return Retry(retry_checker, max_retries, func, sleep_multiplier,
122 retry_backoff_factor, *args, **kwargs)
123
124 return _FunctionWrapper
125
126 return _Wrapper
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700127
128
cylan4f73c1f2018-07-19 16:40:31 +0800129def Retry(retry_checker, max_retries, functor, sleep_multiplier,
130 retry_backoff_factor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000131 """Conditionally retry a function.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700132
cylan0d77ae12018-05-18 08:36:48 +0000133 Args:
134 retry_checker: A callback function which should take an exception instance
135 and return True if functor(*args, **kwargs) should be retried
136 when such exception is raised, and return False if it should
137 not be retried.
138 max_retries: Maximum number of retries allowed.
139 functor: The function to call, will call functor(*args, **kwargs).
140 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
141 retry_backoff_factor is 1. Will sleep
142 sleep_multiplier * (
143 retry_backoff_factor ** (attempt_count - 1))
144 if retry_backoff_factor != 1.
145 retry_backoff_factor: See explanation of sleep_multiplier.
146 *args: Arguments to pass to the functor.
147 **kwargs: Key-val based arguments to pass to the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700148
cylan0d77ae12018-05-18 08:36:48 +0000149 Returns:
150 The return value of the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700151
cylan0d77ae12018-05-18 08:36:48 +0000152 Raises:
153 Exception: The exception that functor(*args, **kwargs) throws.
154 """
155 attempt_count = 0
156 while attempt_count <= max_retries:
157 try:
158 attempt_count += 1
159 return_value = functor(*args, **kwargs)
160 return return_value
161 except Exception as e: # pylint: disable=W0703
162 if retry_checker(e) and attempt_count <= max_retries:
163 if retry_backoff_factor != 1:
164 sleep = sleep_multiplier * (retry_backoff_factor**
165 (attempt_count - 1))
166 else:
167 sleep = sleep_multiplier * attempt_count
Kevin Chengd25feee2018-05-24 10:15:20 -0700168 time.sleep(sleep)
cylan0d77ae12018-05-18 08:36:48 +0000169 else:
170 raise
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700171
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700172
Fang Dengf24be082018-02-10 10:09:55 -0800173def RetryExceptionType(exception_types, max_retries, functor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000174 """Retry exception if it is one of the given types.
Fang Dengf24be082018-02-10 10:09:55 -0800175
cylan0d77ae12018-05-18 08:36:48 +0000176 Args:
177 exception_types: A tuple of exception types, e.g. (ValueError, KeyError)
178 max_retries: Max number of retries allowed.
179 functor: The function to call. Will be retried if exception is raised and
180 the exception is one of the exception_types.
181 *args: Arguments to pass to Retry function.
182 **kwargs: Key-val based arguments to pass to Retry functions.
Fang Dengf24be082018-02-10 10:09:55 -0800183
cylan0d77ae12018-05-18 08:36:48 +0000184 Returns:
185 The value returned by calling functor.
186 """
187 return Retry(lambda e: isinstance(e, exception_types), max_retries,
188 functor, *args, **kwargs)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700189
190
191def PollAndWait(func, expected_return, timeout_exception, timeout_secs,
192 sleep_interval_secs, *args, **kwargs):
193 """Call a function until the function returns expected value or times out.
194
195 Args:
196 func: Function to call.
197 expected_return: The expected return value.
198 timeout_exception: Exception to raise when it hits timeout.
199 timeout_secs: Timeout seconds.
200 If 0 or less than zero, the function will run once and
201 we will not wait on it.
202 sleep_interval_secs: Time to sleep between two attemps.
203 *args: list of args to pass to func.
204 **kwargs: dictionary of keyword based args to pass to func.
205
206 Raises:
207 timeout_exception: if the run of function times out.
208 """
209 # TODO(fdeng): Currently this method does not kill
210 # |func|, if |func| takes longer than |timeout_secs|.
211 # We can use a more robust version from chromite.
212 start = time.time()
213 while True:
214 return_value = func(*args, **kwargs)
215 if return_value == expected_return:
216 return
217 elif time.time() - start > timeout_secs:
218 raise timeout_exception
219 else:
220 if sleep_interval_secs > 0:
221 time.sleep(sleep_interval_secs)
222
223
224def GenerateUniqueName(prefix=None, suffix=None):
Sam Chiu81bdc652018-06-29 18:45:08 +0800225 """Generate a random unique name using uuid4.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700226
227 Args:
228 prefix: String, desired prefix to prepend to the generated name.
229 suffix: String, desired suffix to append to the generated name.
230
231 Returns:
232 String, a random name.
233 """
234 name = uuid.uuid4().hex
235 if prefix:
236 name = "-".join([prefix, name])
237 if suffix:
238 name = "-".join([name, suffix])
239 return name
240
241
242def MakeTarFile(src_dict, dest):
243 """Archive files in tar.gz format to a file named as |dest|.
244
245 Args:
246 src_dict: A dictionary that maps a path to be archived
247 to the corresponding name that appears in the archive.
248 dest: String, path to output file, e.g. /tmp/myfile.tar.gz
249 """
250 logger.info("Compressing %s into %s.", src_dict.keys(), dest)
251 with tarfile.open(dest, "w:gz") as tar:
252 for src, arcname in src_dict.iteritems():
253 tar.add(src, arcname=arcname)
254
255
Fang Deng69498c32017-03-02 14:29:30 -0800256def CreateSshKeyPairIfNotExist(private_key_path, public_key_path):
257 """Create the ssh key pair if they don't exist.
258
cylan4f73c1f2018-07-19 16:40:31 +0800259 Case1. If the private key doesn't exist, we will create both the public key
260 and the private key.
261 Case2. If the private key exists but public key doesn't, we will create the
262 public key by using the private key.
263 Case3. If the public key exists but the private key doesn't, we will create
264 a new private key and overwrite the public key.
Fang Deng69498c32017-03-02 14:29:30 -0800265
266 Args:
267 private_key_path: Path to the private key file.
268 e.g. ~/.ssh/acloud_rsa
269 public_key_path: Path to the public key file.
270 e.g. ~/.ssh/acloud_rsa.pub
cylan4f73c1f2018-07-19 16:40:31 +0800271
Fang Deng69498c32017-03-02 14:29:30 -0800272 Raises:
273 error.DriverError: If failed to create the key pair.
274 """
275 public_key_path = os.path.expanduser(public_key_path)
276 private_key_path = os.path.expanduser(private_key_path)
cylan4f73c1f2018-07-19 16:40:31 +0800277 public_key_exist = os.path.exists(public_key_path)
278 private_key_exist = os.path.exists(private_key_path)
279 if public_key_exist and private_key_exist:
cylan0d77ae12018-05-18 08:36:48 +0000280 logger.debug(
cylan4f73c1f2018-07-19 16:40:31 +0800281 "The ssh private key (%s) and public key (%s) already exist,"
cylan0d77ae12018-05-18 08:36:48 +0000282 "will not automatically create the key pairs.", private_key_path,
283 public_key_path)
Fang Deng69498c32017-03-02 14:29:30 -0800284 return
cylan4f73c1f2018-07-19 16:40:31 +0800285 key_folder = os.path.dirname(private_key_path)
286 if not os.path.exists(key_folder):
287 os.makedirs(key_folder)
Fang Deng69498c32017-03-02 14:29:30 -0800288 try:
cylan4f73c1f2018-07-19 16:40:31 +0800289 if private_key_exist:
290 cmd = SSH_KEYGEN_PUB_CMD + ["-f", private_key_path]
291 with open(public_key_path, 'w') as outfile:
292 stream_content = subprocess.check_output(cmd)
293 outfile.write(
294 stream_content.rstrip('\n') + " " + getpass.getuser())
295 logger.info(
296 "The ssh public key (%s) do not exist, "
297 "automatically creating public key, calling: %s",
298 public_key_path, " ".join(cmd))
299 else:
300 cmd = SSH_KEYGEN_CMD + [
301 "-C", getpass.getuser(), "-f", private_key_path
302 ]
303 logger.info(
304 "Creating public key from private key (%s) via cmd: %s",
305 private_key_path, " ".join(cmd))
306 subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout)
Fang Deng69498c32017-03-02 14:29:30 -0800307 except subprocess.CalledProcessError as e:
cylan0d77ae12018-05-18 08:36:48 +0000308 raise errors.DriverError("Failed to create ssh key pair: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800309 except OSError as e:
310 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000311 "Failed to create ssh key pair, please make sure "
312 "'ssh-keygen' is installed: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800313
314 # By default ssh-keygen will create a public key file
315 # by append .pub to the private key file name. Rename it
316 # to what's requested by public_key_path.
317 default_pub_key_path = "%s.pub" % private_key_path
318 try:
319 if default_pub_key_path != public_key_path:
320 os.rename(default_pub_key_path, public_key_path)
321 except OSError as e:
322 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000323 "Failed to rename %s to %s: %s" % (default_pub_key_path,
324 public_key_path, str(e)))
Fang Deng69498c32017-03-02 14:29:30 -0800325
326 logger.info("Created ssh private key (%s) and public key (%s)",
327 private_key_path, public_key_path)
328
329
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700330def VerifyRsaPubKey(rsa):
331 """Verify the format of rsa public key.
332
333 Args:
334 rsa: content of rsa public key. It should follow the format of
335 ssh-rsa AAAAB3NzaC1yc2EA.... test@test.com
336
337 Raises:
338 DriverError if the format is not correct.
339 """
340 if not rsa or not all(ord(c) < 128 for c in rsa):
341 raise errors.DriverError(
342 "rsa key is empty or contains non-ascii character: %s" % rsa)
343
344 elements = rsa.split()
345 if len(elements) != 3:
346 raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa)
347
348 key_type, data, _ = elements
349 try:
350 binary_data = base64.decodestring(data)
351 # number of bytes of int type
352 int_length = 4
353 # binary_data is like "7ssh-key..." in a binary format.
354 # The first 4 bytes should represent 7, which should be
355 # the length of the following string "ssh-key".
356 # And the next 7 bytes should be string "ssh-key".
357 # We will verify that the rsa conforms to this format.
358 # ">I" in the following line means "big-endian unsigned integer".
359 type_length = struct.unpack(">I", binary_data[:int_length])[0]
360 if binary_data[int_length:int_length + type_length] != key_type:
361 raise errors.DriverError("rsa key is invalid: %s" % rsa)
362 except (struct.error, binascii.Error) as e:
cylan0d77ae12018-05-18 08:36:48 +0000363 raise errors.DriverError(
364 "rsa key is invalid: %s, error: %s" % (rsa, str(e)))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700365
366
Sam Chiu81bdc652018-06-29 18:45:08 +0800367# pylint: disable=old-style-class,no-init
368class TextColors:
369 """A class that defines common color ANSI code."""
370
371 HEADER = "\033[95m"
372 OKBLUE = "\033[94m"
373 OKGREEN = "\033[92m"
374 WARNING = "\033[93m"
375 FAIL = "\033[91m"
376 ENDC = "\033[0m"
377 BOLD = "\033[1m"
378 UNDERLINE = "\033[4m"
379
380
herbertxuedf01c422018-09-06 19:52:52 +0800381def PrintColorString(message, colors=TextColors.OKBLUE, **kwargs):
Sam Chiu81bdc652018-06-29 18:45:08 +0800382 """A helper function to print out colored text.
383
herbertxuedf01c422018-09-06 19:52:52 +0800384 Use print function "print(message, end="")" to show message in one line.
385 Example code:
386 DisplayMessages("Creating GCE instance...", end="")
387 # Job execute 20s
388 DisplayMessages("Done! (20s)")
389 Display:
390 Creating GCE instance...
391 # After job finished, messages update as following:
392 Creating GCE instance...Done! (20s)
393
Sam Chiu81bdc652018-06-29 18:45:08 +0800394 Args:
395 message: String, the message text.
396 colors: String, color code.
herbertxuedf01c422018-09-06 19:52:52 +0800397 **kwargs: dictionary of keyword based args to pass to func.
Sam Chiu81bdc652018-06-29 18:45:08 +0800398 """
herbertxuedf01c422018-09-06 19:52:52 +0800399 print(colors + message + TextColors.ENDC, **kwargs)
400 sys.stdout.flush()
Sam Chiu81bdc652018-06-29 18:45:08 +0800401
402
403def InteractWithQuestion(question, colors=TextColors.WARNING):
404 """A helper function to define the common way to run interactive cmd.
405
406 Args:
407 question: String, the question to ask user.
408 colors: String, color code.
409
410 Returns:
411 String, input from user.
412 """
413 return str(raw_input(colors + question + TextColors.ENDC).strip())
414
herbertxuedf01c422018-09-06 19:52:52 +0800415
herbertxue34776bb2018-07-03 21:57:48 +0800416def GetUserAnswerYes(question):
417 """Ask user about acloud setup question.
418
419 Args:
420 question: String, ask question for user.
421 Ex: "Are you sure to change bucket name:[y/n]"
422
423 Returns:
424 Boolean, True if answer is "Yes", False otherwise.
425 """
426 answer = InteractWithQuestion(question)
427 return answer.lower() in constants.USER_ANSWER_YES
428
Sam Chiu81bdc652018-06-29 18:45:08 +0800429
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700430class BatchHttpRequestExecutor(object):
431 """A helper class that executes requests in batch with retry.
432
433 This executor executes http requests in a batch and retry
434 those that have failed. It iteratively updates the dictionary
435 self._final_results with latest results, which can be retrieved
436 via GetResults.
437 """
438
439 def __init__(self,
440 execute_once_functor,
441 requests,
442 retry_http_codes=None,
443 max_retry=None,
444 sleep=None,
445 backoff_factor=None,
446 other_retriable_errors=None):
447 """Initializes the executor.
448
449 Args:
450 execute_once_functor: A function that execute requests in batch once.
451 It should return a dictionary like
452 {request_id: (response, exception)}
453 requests: A dictionary where key is request id picked by caller,
454 and value is a apiclient.http.HttpRequest.
455 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800456 max_retry: See utils.Retry.
457 sleep: See utils.Retry.
458 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700459 other_retriable_errors: A tuple of error types that should be retried
460 other than errors.HttpError.
461 """
462 self._execute_once_functor = execute_once_functor
463 self._requests = requests
464 # A dictionary that maps request id to pending request.
465 self._pending_requests = {}
466 # A dictionary that maps request id to a tuple (response, exception).
467 self._final_results = {}
468 self._retry_http_codes = retry_http_codes
469 self._max_retry = max_retry
470 self._sleep = sleep
471 self._backoff_factor = backoff_factor
472 self._other_retriable_errors = other_retriable_errors
473
474 def _ShoudRetry(self, exception):
Sam Chiu81bdc652018-06-29 18:45:08 +0800475 """Check if an exception is retriable.
476
477 Args:
478 exception: An exception instance.
479 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700480 if isinstance(exception, self._other_retriable_errors):
481 return True
482
cylan0d77ae12018-05-18 08:36:48 +0000483 if (isinstance(exception, errors.HttpError)
484 and exception.code in self._retry_http_codes):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700485 return True
486 return False
487
488 def _ExecuteOnce(self):
489 """Executes pending requests and update it with failed, retriable ones.
490
491 Raises:
492 HasRetriableRequestsError: if some requests fail and are retriable.
493 """
494 results = self._execute_once_functor(self._pending_requests)
495 # Update final_results with latest results.
496 self._final_results.update(results)
497 # Clear pending_requests
498 self._pending_requests.clear()
499 for request_id, result in results.iteritems():
500 exception = result[1]
501 if exception is not None and self._ShoudRetry(exception):
502 # If this is a retriable exception, put it in pending_requests
503 self._pending_requests[request_id] = self._requests[request_id]
504 if self._pending_requests:
505 # If there is still retriable requests pending, raise an error
Fang Dengf24be082018-02-10 10:09:55 -0800506 # so that Retry will retry this function with pending_requests.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700507 raise errors.HasRetriableRequestsError(
cylan0d77ae12018-05-18 08:36:48 +0000508 "Retriable errors: %s" %
509 [str(results[rid][1]) for rid in self._pending_requests])
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700510
511 def Execute(self):
512 """Executes the requests and retry if necessary.
513
514 Will populate self._final_results.
515 """
cylan0d77ae12018-05-18 08:36:48 +0000516
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700517 def _ShouldRetryHandler(exc):
518 """Check if |exc| is a retriable exception.
519
520 Args:
521 exc: An exception.
522
523 Returns:
524 True if exception is of type HasRetriableRequestsError; False otherwise.
525 """
526 should_retry = isinstance(exc, errors.HasRetriableRequestsError)
527 if should_retry:
528 logger.info("Will retry failed requests.", exc_info=True)
529 logger.info("%s", exc)
530 return should_retry
531
532 try:
533 self._pending_requests = self._requests.copy()
Fang Dengf24be082018-02-10 10:09:55 -0800534 Retry(
cylan0d77ae12018-05-18 08:36:48 +0000535 _ShouldRetryHandler,
536 max_retries=self._max_retry,
Fang Dengf24be082018-02-10 10:09:55 -0800537 functor=self._ExecuteOnce,
538 sleep_multiplier=self._sleep,
539 retry_backoff_factor=self._backoff_factor)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700540 except errors.HasRetriableRequestsError:
541 logger.debug("Some requests did not succeed after retry.")
542
543 def GetResults(self):
544 """Returns final results.
545
546 Returns:
547 results, a dictionary in the following format
548 {request_id: (response, exception)}
549 request_ids are those from requests; response
550 is the http response for the request or None on error;
551 exception is an instance of DriverError or None if no error.
552 """
553 return self._final_results