blob: 241c3c807c94a9fd716e7f7badbbd2857f29391a [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
chojoycecd004bc2018-09-13 10:39:00 +080033import zipfile
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070034
chojoycecd004bc2018-09-13 10:39:00 +080035from acloud import errors as root_errors
herbertxue34776bb2018-07-03 21:57:48 +080036from acloud.internal import constants
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070037from acloud.public import errors
38
39logger = logging.getLogger(__name__)
40
Fang Deng69498c32017-03-02 14:29:30 -080041SSH_KEYGEN_CMD = ["ssh-keygen", "-t", "rsa", "-b", "4096"]
cylan4f73c1f2018-07-19 16:40:31 +080042SSH_KEYGEN_PUB_CMD = ["ssh-keygen", "-y"]
Kevin Chengd25feee2018-05-24 10:15:20 -070043DEFAULT_RETRY_BACKOFF_FACTOR = 1
44DEFAULT_SLEEP_MULTIPLIER = 0
Fang Deng69498c32017-03-02 14:29:30 -080045
46
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070047class TempDir(object):
Fang Deng26e4dc12018-03-04 19:01:59 -080048 """A context manager that ceates a temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070049
Fang Deng26e4dc12018-03-04 19:01:59 -080050 Attributes:
Sam Chiu81bdc652018-06-29 18:45:08 +080051 path: The path of the temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070052 """
53
Fang Deng26e4dc12018-03-04 19:01:59 -080054 def __init__(self):
55 self.path = tempfile.mkdtemp()
56 os.chmod(self.path, 0o700)
57 logger.debug("Created temporary dir %s", self.path)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070058
59 def __enter__(self):
Fang Deng26e4dc12018-03-04 19:01:59 -080060 """Enter."""
61 return self.path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070062
Fang Deng26e4dc12018-03-04 19:01:59 -080063 def __exit__(self, exc_type, exc_value, traceback):
64 """Exit.
65
66 Args:
67 exc_type: Exception type raised within the context manager.
68 None if no execption is raised.
69 exc_value: Exception instance raised within the context manager.
70 None if no execption is raised.
71 traceback: Traceback for exeception that is raised within
72 the context manager.
73 None if no execption is raised.
74 Raises:
75 EnvironmentError or OSError when failed to delete temp directory.
76 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070077 try:
Fang Deng26e4dc12018-03-04 19:01:59 -080078 if self.path:
79 shutil.rmtree(self.path)
80 logger.debug("Deleted temporary dir %s", self.path)
81 except EnvironmentError as e:
82 # Ignore error if there is no exception raised
83 # within the with-clause and the EnvironementError is
84 # about problem that directory or file does not exist.
85 if not exc_type and e.errno != errno.ENOENT:
86 raise
87 except Exception as e: # pylint: disable=W0703
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070088 if exc_type:
Fang Deng26e4dc12018-03-04 19:01:59 -080089 logger.error(
cylan0d77ae12018-05-18 08:36:48 +000090 "Encountered error while deleting %s: %s",
91 self.path,
92 str(e),
93 exc_info=True)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070094 else:
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070095 raise
96
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070097
cylan0d77ae12018-05-18 08:36:48 +000098def RetryOnException(retry_checker,
99 max_retries,
100 sleep_multiplier=0,
Fang Dengf24be082018-02-10 10:09:55 -0800101 retry_backoff_factor=1):
cylan0d77ae12018-05-18 08:36:48 +0000102 """Decorater which retries the function call if |retry_checker| returns true.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700103
cylan0d77ae12018-05-18 08:36:48 +0000104 Args:
105 retry_checker: A callback function which should take an exception instance
106 and return True if functor(*args, **kwargs) should be retried
107 when such exception is raised, and return False if it should
108 not be retried.
109 max_retries: Maximum number of retries allowed.
110 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
111 retry_backoff_factor is 1. Will sleep
112 sleep_multiplier * (
113 retry_backoff_factor ** (attempt_count - 1))
114 if retry_backoff_factor != 1.
115 retry_backoff_factor: See explanation of sleep_multiplier.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700116
cylan0d77ae12018-05-18 08:36:48 +0000117 Returns:
118 The function wrapper.
119 """
120
121 def _Wrapper(func):
122 def _FunctionWrapper(*args, **kwargs):
123 return Retry(retry_checker, max_retries, func, sleep_multiplier,
124 retry_backoff_factor, *args, **kwargs)
125
126 return _FunctionWrapper
127
128 return _Wrapper
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700129
130
cylan4f73c1f2018-07-19 16:40:31 +0800131def Retry(retry_checker, max_retries, functor, sleep_multiplier,
132 retry_backoff_factor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000133 """Conditionally retry a function.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700134
cylan0d77ae12018-05-18 08:36:48 +0000135 Args:
136 retry_checker: A callback function which should take an exception instance
137 and return True if functor(*args, **kwargs) should be retried
138 when such exception is raised, and return False if it should
139 not be retried.
140 max_retries: Maximum number of retries allowed.
141 functor: The function to call, will call functor(*args, **kwargs).
142 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
143 retry_backoff_factor is 1. Will sleep
144 sleep_multiplier * (
145 retry_backoff_factor ** (attempt_count - 1))
146 if retry_backoff_factor != 1.
147 retry_backoff_factor: See explanation of sleep_multiplier.
148 *args: Arguments to pass to the functor.
149 **kwargs: Key-val based arguments to pass to the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700150
cylan0d77ae12018-05-18 08:36:48 +0000151 Returns:
152 The return value of the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700153
cylan0d77ae12018-05-18 08:36:48 +0000154 Raises:
155 Exception: The exception that functor(*args, **kwargs) throws.
156 """
157 attempt_count = 0
158 while attempt_count <= max_retries:
159 try:
160 attempt_count += 1
161 return_value = functor(*args, **kwargs)
162 return return_value
163 except Exception as e: # pylint: disable=W0703
164 if retry_checker(e) and attempt_count <= max_retries:
165 if retry_backoff_factor != 1:
166 sleep = sleep_multiplier * (retry_backoff_factor**
167 (attempt_count - 1))
168 else:
169 sleep = sleep_multiplier * attempt_count
Kevin Chengd25feee2018-05-24 10:15:20 -0700170 time.sleep(sleep)
cylan0d77ae12018-05-18 08:36:48 +0000171 else:
172 raise
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700173
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700174
Fang Dengf24be082018-02-10 10:09:55 -0800175def RetryExceptionType(exception_types, max_retries, functor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000176 """Retry exception if it is one of the given types.
Fang Dengf24be082018-02-10 10:09:55 -0800177
cylan0d77ae12018-05-18 08:36:48 +0000178 Args:
179 exception_types: A tuple of exception types, e.g. (ValueError, KeyError)
180 max_retries: Max number of retries allowed.
181 functor: The function to call. Will be retried if exception is raised and
182 the exception is one of the exception_types.
183 *args: Arguments to pass to Retry function.
184 **kwargs: Key-val based arguments to pass to Retry functions.
Fang Dengf24be082018-02-10 10:09:55 -0800185
cylan0d77ae12018-05-18 08:36:48 +0000186 Returns:
187 The value returned by calling functor.
188 """
189 return Retry(lambda e: isinstance(e, exception_types), max_retries,
190 functor, *args, **kwargs)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700191
192
193def PollAndWait(func, expected_return, timeout_exception, timeout_secs,
194 sleep_interval_secs, *args, **kwargs):
195 """Call a function until the function returns expected value or times out.
196
197 Args:
198 func: Function to call.
199 expected_return: The expected return value.
200 timeout_exception: Exception to raise when it hits timeout.
201 timeout_secs: Timeout seconds.
202 If 0 or less than zero, the function will run once and
203 we will not wait on it.
204 sleep_interval_secs: Time to sleep between two attemps.
205 *args: list of args to pass to func.
206 **kwargs: dictionary of keyword based args to pass to func.
207
208 Raises:
209 timeout_exception: if the run of function times out.
210 """
211 # TODO(fdeng): Currently this method does not kill
212 # |func|, if |func| takes longer than |timeout_secs|.
213 # We can use a more robust version from chromite.
214 start = time.time()
215 while True:
216 return_value = func(*args, **kwargs)
217 if return_value == expected_return:
218 return
219 elif time.time() - start > timeout_secs:
220 raise timeout_exception
221 else:
222 if sleep_interval_secs > 0:
223 time.sleep(sleep_interval_secs)
224
225
226def GenerateUniqueName(prefix=None, suffix=None):
Sam Chiu81bdc652018-06-29 18:45:08 +0800227 """Generate a random unique name using uuid4.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700228
229 Args:
230 prefix: String, desired prefix to prepend to the generated name.
231 suffix: String, desired suffix to append to the generated name.
232
233 Returns:
234 String, a random name.
235 """
236 name = uuid.uuid4().hex
237 if prefix:
238 name = "-".join([prefix, name])
239 if suffix:
240 name = "-".join([name, suffix])
241 return name
242
243
244def MakeTarFile(src_dict, dest):
245 """Archive files in tar.gz format to a file named as |dest|.
246
247 Args:
248 src_dict: A dictionary that maps a path to be archived
249 to the corresponding name that appears in the archive.
250 dest: String, path to output file, e.g. /tmp/myfile.tar.gz
251 """
252 logger.info("Compressing %s into %s.", src_dict.keys(), dest)
253 with tarfile.open(dest, "w:gz") as tar:
254 for src, arcname in src_dict.iteritems():
255 tar.add(src, arcname=arcname)
256
257
Fang Deng69498c32017-03-02 14:29:30 -0800258def CreateSshKeyPairIfNotExist(private_key_path, public_key_path):
259 """Create the ssh key pair if they don't exist.
260
cylan4f73c1f2018-07-19 16:40:31 +0800261 Case1. If the private key doesn't exist, we will create both the public key
262 and the private key.
263 Case2. If the private key exists but public key doesn't, we will create the
264 public key by using the private key.
265 Case3. If the public key exists but the private key doesn't, we will create
266 a new private key and overwrite the public key.
Fang Deng69498c32017-03-02 14:29:30 -0800267
268 Args:
269 private_key_path: Path to the private key file.
270 e.g. ~/.ssh/acloud_rsa
271 public_key_path: Path to the public key file.
272 e.g. ~/.ssh/acloud_rsa.pub
cylan4f73c1f2018-07-19 16:40:31 +0800273
Fang Deng69498c32017-03-02 14:29:30 -0800274 Raises:
275 error.DriverError: If failed to create the key pair.
276 """
277 public_key_path = os.path.expanduser(public_key_path)
278 private_key_path = os.path.expanduser(private_key_path)
cylan4f73c1f2018-07-19 16:40:31 +0800279 public_key_exist = os.path.exists(public_key_path)
280 private_key_exist = os.path.exists(private_key_path)
281 if public_key_exist and private_key_exist:
cylan0d77ae12018-05-18 08:36:48 +0000282 logger.debug(
cylan4f73c1f2018-07-19 16:40:31 +0800283 "The ssh private key (%s) and public key (%s) already exist,"
cylan0d77ae12018-05-18 08:36:48 +0000284 "will not automatically create the key pairs.", private_key_path,
285 public_key_path)
Fang Deng69498c32017-03-02 14:29:30 -0800286 return
cylan4f73c1f2018-07-19 16:40:31 +0800287 key_folder = os.path.dirname(private_key_path)
288 if not os.path.exists(key_folder):
289 os.makedirs(key_folder)
Fang Deng69498c32017-03-02 14:29:30 -0800290 try:
cylan4f73c1f2018-07-19 16:40:31 +0800291 if private_key_exist:
292 cmd = SSH_KEYGEN_PUB_CMD + ["-f", private_key_path]
293 with open(public_key_path, 'w') as outfile:
294 stream_content = subprocess.check_output(cmd)
295 outfile.write(
296 stream_content.rstrip('\n') + " " + getpass.getuser())
297 logger.info(
298 "The ssh public key (%s) do not exist, "
299 "automatically creating public key, calling: %s",
300 public_key_path, " ".join(cmd))
301 else:
302 cmd = SSH_KEYGEN_CMD + [
303 "-C", getpass.getuser(), "-f", private_key_path
304 ]
305 logger.info(
306 "Creating public key from private key (%s) via cmd: %s",
307 private_key_path, " ".join(cmd))
308 subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout)
Fang Deng69498c32017-03-02 14:29:30 -0800309 except subprocess.CalledProcessError as e:
cylan0d77ae12018-05-18 08:36:48 +0000310 raise errors.DriverError("Failed to create ssh key pair: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800311 except OSError as e:
312 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000313 "Failed to create ssh key pair, please make sure "
314 "'ssh-keygen' is installed: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800315
316 # By default ssh-keygen will create a public key file
317 # by append .pub to the private key file name. Rename it
318 # to what's requested by public_key_path.
319 default_pub_key_path = "%s.pub" % private_key_path
320 try:
321 if default_pub_key_path != public_key_path:
322 os.rename(default_pub_key_path, public_key_path)
323 except OSError as e:
324 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000325 "Failed to rename %s to %s: %s" % (default_pub_key_path,
326 public_key_path, str(e)))
Fang Deng69498c32017-03-02 14:29:30 -0800327
328 logger.info("Created ssh private key (%s) and public key (%s)",
329 private_key_path, public_key_path)
330
331
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700332def VerifyRsaPubKey(rsa):
333 """Verify the format of rsa public key.
334
335 Args:
336 rsa: content of rsa public key. It should follow the format of
337 ssh-rsa AAAAB3NzaC1yc2EA.... test@test.com
338
339 Raises:
340 DriverError if the format is not correct.
341 """
342 if not rsa or not all(ord(c) < 128 for c in rsa):
343 raise errors.DriverError(
344 "rsa key is empty or contains non-ascii character: %s" % rsa)
345
346 elements = rsa.split()
347 if len(elements) != 3:
348 raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa)
349
350 key_type, data, _ = elements
351 try:
352 binary_data = base64.decodestring(data)
353 # number of bytes of int type
354 int_length = 4
355 # binary_data is like "7ssh-key..." in a binary format.
356 # The first 4 bytes should represent 7, which should be
357 # the length of the following string "ssh-key".
358 # And the next 7 bytes should be string "ssh-key".
359 # We will verify that the rsa conforms to this format.
360 # ">I" in the following line means "big-endian unsigned integer".
361 type_length = struct.unpack(">I", binary_data[:int_length])[0]
362 if binary_data[int_length:int_length + type_length] != key_type:
363 raise errors.DriverError("rsa key is invalid: %s" % rsa)
364 except (struct.error, binascii.Error) as e:
cylan0d77ae12018-05-18 08:36:48 +0000365 raise errors.DriverError(
366 "rsa key is invalid: %s, error: %s" % (rsa, str(e)))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700367
chojoycecd004bc2018-09-13 10:39:00 +0800368def Decompress(sourcefile, dest=None):
369 """Decompress .zip or .tar.gz.
370
371 Args:
372 sourcefile: A string, a source file path to decompress.
373 dest: A string, a folder path as decompress destination.
374
375 Raises:
376 errors.UnsupportedCompressionFileType: Not supported extension.
377 """
378 logger.info("Start to decompress %s!", sourcefile)
379 dest_path = dest if dest else "."
380 if sourcefile.endswith(".tar.gz"):
381 with tarfile.open(sourcefile, "r:gz") as compressor:
382 compressor.extractall(dest_path)
383 elif sourcefile.endswith(".zip"):
384 with zipfile.ZipFile(sourcefile, 'r') as compressor:
385 compressor.extractall(dest_path)
386 else:
387 raise root_errors.UnsupportedCompressionFileType(
388 "Sorry, we could only support compression file type "
389 "for zip or tar.gz.")
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700390
Sam Chiu81bdc652018-06-29 18:45:08 +0800391# pylint: disable=old-style-class,no-init
392class TextColors:
393 """A class that defines common color ANSI code."""
394
395 HEADER = "\033[95m"
396 OKBLUE = "\033[94m"
397 OKGREEN = "\033[92m"
398 WARNING = "\033[93m"
399 FAIL = "\033[91m"
400 ENDC = "\033[0m"
401 BOLD = "\033[1m"
402 UNDERLINE = "\033[4m"
403
404
herbertxuedf01c422018-09-06 19:52:52 +0800405def PrintColorString(message, colors=TextColors.OKBLUE, **kwargs):
Sam Chiu81bdc652018-06-29 18:45:08 +0800406 """A helper function to print out colored text.
407
herbertxuedf01c422018-09-06 19:52:52 +0800408 Use print function "print(message, end="")" to show message in one line.
409 Example code:
410 DisplayMessages("Creating GCE instance...", end="")
411 # Job execute 20s
412 DisplayMessages("Done! (20s)")
413 Display:
414 Creating GCE instance...
415 # After job finished, messages update as following:
416 Creating GCE instance...Done! (20s)
417
Sam Chiu81bdc652018-06-29 18:45:08 +0800418 Args:
419 message: String, the message text.
420 colors: String, color code.
herbertxuedf01c422018-09-06 19:52:52 +0800421 **kwargs: dictionary of keyword based args to pass to func.
Sam Chiu81bdc652018-06-29 18:45:08 +0800422 """
herbertxuedf01c422018-09-06 19:52:52 +0800423 print(colors + message + TextColors.ENDC, **kwargs)
424 sys.stdout.flush()
Sam Chiu81bdc652018-06-29 18:45:08 +0800425
426
427def InteractWithQuestion(question, colors=TextColors.WARNING):
428 """A helper function to define the common way to run interactive cmd.
429
430 Args:
431 question: String, the question to ask user.
432 colors: String, color code.
433
434 Returns:
435 String, input from user.
436 """
437 return str(raw_input(colors + question + TextColors.ENDC).strip())
438
herbertxuedf01c422018-09-06 19:52:52 +0800439
herbertxue34776bb2018-07-03 21:57:48 +0800440def GetUserAnswerYes(question):
441 """Ask user about acloud setup question.
442
443 Args:
444 question: String, ask question for user.
445 Ex: "Are you sure to change bucket name:[y/n]"
446
447 Returns:
448 Boolean, True if answer is "Yes", False otherwise.
449 """
450 answer = InteractWithQuestion(question)
451 return answer.lower() in constants.USER_ANSWER_YES
452
Sam Chiu81bdc652018-06-29 18:45:08 +0800453
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700454class BatchHttpRequestExecutor(object):
455 """A helper class that executes requests in batch with retry.
456
457 This executor executes http requests in a batch and retry
458 those that have failed. It iteratively updates the dictionary
459 self._final_results with latest results, which can be retrieved
460 via GetResults.
461 """
462
463 def __init__(self,
464 execute_once_functor,
465 requests,
466 retry_http_codes=None,
467 max_retry=None,
468 sleep=None,
469 backoff_factor=None,
470 other_retriable_errors=None):
471 """Initializes the executor.
472
473 Args:
474 execute_once_functor: A function that execute requests in batch once.
475 It should return a dictionary like
476 {request_id: (response, exception)}
477 requests: A dictionary where key is request id picked by caller,
478 and value is a apiclient.http.HttpRequest.
479 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800480 max_retry: See utils.Retry.
481 sleep: See utils.Retry.
482 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700483 other_retriable_errors: A tuple of error types that should be retried
484 other than errors.HttpError.
485 """
486 self._execute_once_functor = execute_once_functor
487 self._requests = requests
488 # A dictionary that maps request id to pending request.
489 self._pending_requests = {}
490 # A dictionary that maps request id to a tuple (response, exception).
491 self._final_results = {}
492 self._retry_http_codes = retry_http_codes
493 self._max_retry = max_retry
494 self._sleep = sleep
495 self._backoff_factor = backoff_factor
496 self._other_retriable_errors = other_retriable_errors
497
498 def _ShoudRetry(self, exception):
Sam Chiu81bdc652018-06-29 18:45:08 +0800499 """Check if an exception is retriable.
500
501 Args:
502 exception: An exception instance.
503 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700504 if isinstance(exception, self._other_retriable_errors):
505 return True
506
cylan0d77ae12018-05-18 08:36:48 +0000507 if (isinstance(exception, errors.HttpError)
508 and exception.code in self._retry_http_codes):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700509 return True
510 return False
511
512 def _ExecuteOnce(self):
513 """Executes pending requests and update it with failed, retriable ones.
514
515 Raises:
516 HasRetriableRequestsError: if some requests fail and are retriable.
517 """
518 results = self._execute_once_functor(self._pending_requests)
519 # Update final_results with latest results.
520 self._final_results.update(results)
521 # Clear pending_requests
522 self._pending_requests.clear()
523 for request_id, result in results.iteritems():
524 exception = result[1]
525 if exception is not None and self._ShoudRetry(exception):
526 # If this is a retriable exception, put it in pending_requests
527 self._pending_requests[request_id] = self._requests[request_id]
528 if self._pending_requests:
529 # If there is still retriable requests pending, raise an error
Fang Dengf24be082018-02-10 10:09:55 -0800530 # so that Retry will retry this function with pending_requests.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700531 raise errors.HasRetriableRequestsError(
cylan0d77ae12018-05-18 08:36:48 +0000532 "Retriable errors: %s" %
533 [str(results[rid][1]) for rid in self._pending_requests])
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700534
535 def Execute(self):
536 """Executes the requests and retry if necessary.
537
538 Will populate self._final_results.
539 """
cylan0d77ae12018-05-18 08:36:48 +0000540
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700541 def _ShouldRetryHandler(exc):
542 """Check if |exc| is a retriable exception.
543
544 Args:
545 exc: An exception.
546
547 Returns:
548 True if exception is of type HasRetriableRequestsError; False otherwise.
549 """
550 should_retry = isinstance(exc, errors.HasRetriableRequestsError)
551 if should_retry:
552 logger.info("Will retry failed requests.", exc_info=True)
553 logger.info("%s", exc)
554 return should_retry
555
556 try:
557 self._pending_requests = self._requests.copy()
Fang Dengf24be082018-02-10 10:09:55 -0800558 Retry(
cylan0d77ae12018-05-18 08:36:48 +0000559 _ShouldRetryHandler,
560 max_retries=self._max_retry,
Fang Dengf24be082018-02-10 10:09:55 -0800561 functor=self._ExecuteOnce,
562 sleep_multiplier=self._sleep,
563 retry_backoff_factor=self._backoff_factor)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700564 except errors.HasRetriableRequestsError:
565 logger.debug("Some requests did not succeed after retry.")
566
567 def GetResults(self):
568 """Returns final results.
569
570 Returns:
571 results, a dictionary in the following format
572 {request_id: (response, exception)}
573 request_ids are those from requests; response
574 is the http response for the request or None on error;
575 exception is an instance of DriverError or None if no error.
576 """
577 return self._final_results
cylan31fc5332018-09-17 22:12:08 +0800578
579
580class TimeExecute(object):
581 """Count the function execute time."""
582
583 def __init__(self, function_description=None, print_before_call=True, print_status=True):
584 """Initializes the class.
585
586 Args:
587 function_description: String that describes function (e.g."Creating
588 Instance...")
589 print_before_call: Boolean, print the function description before
590 calling the function, default True.
591 print_status: Boolean, print the status of the function after the
592 function has completed, default True ("OK" or "Fail").
593 """
594 self._function_description = function_description
595 self._print_before_call = print_before_call
596 self._print_status = print_status
597
598 def __call__(self, func):
599 def DecoratorFunction(*args, **kargs):
600 """Decorator function.
601
602 Args:
603 *args: Arguments to pass to the functor.
604 **kwargs: Key-val based arguments to pass to the functor.
605
606 Raises:
607 Exception: The exception that functor(*args, **kwargs) throws.
608 """
609 timestart = time.time()
610 if self._print_before_call:
611 PrintColorString("%s ..."% self._function_description, end="")
612 try:
613 result = func(*args, **kargs)
614 if not self._print_before_call:
615 PrintColorString("%s (%ds)" % (self._function_description,
616 time.time()-timestart),
617 TextColors.OKGREEN)
618 if self._print_status:
619 PrintColorString("OK! (%ds)" % (time.time()-timestart),
620 TextColors.OKGREEN)
621 return result
622 except:
623 if self._print_status:
624 PrintColorString("Fail! (%ds)" % (time.time()-timestart),
625 TextColors.FAIL)
626 raise
627 return DecoratorFunction