blob: 4d32f8f190dcdd7cf8cf3b93b77945aacdf8197b [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
34from acloud.public import errors
35
36logger = logging.getLogger(__name__)
37
Fang Deng69498c32017-03-02 14:29:30 -080038SSH_KEYGEN_CMD = ["ssh-keygen", "-t", "rsa", "-b", "4096"]
cylan4f73c1f2018-07-19 16:40:31 +080039SSH_KEYGEN_PUB_CMD = ["ssh-keygen", "-y"]
Kevin Chengd25feee2018-05-24 10:15:20 -070040DEFAULT_RETRY_BACKOFF_FACTOR = 1
41DEFAULT_SLEEP_MULTIPLIER = 0
Fang Deng69498c32017-03-02 14:29:30 -080042
43
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070044class TempDir(object):
Fang Deng26e4dc12018-03-04 19:01:59 -080045 """A context manager that ceates a temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070046
Fang Deng26e4dc12018-03-04 19:01:59 -080047 Attributes:
Sam Chiu81bdc652018-06-29 18:45:08 +080048 path: The path of the temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070049 """
50
Fang Deng26e4dc12018-03-04 19:01:59 -080051 def __init__(self):
52 self.path = tempfile.mkdtemp()
53 os.chmod(self.path, 0o700)
54 logger.debug("Created temporary dir %s", self.path)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070055
56 def __enter__(self):
Fang Deng26e4dc12018-03-04 19:01:59 -080057 """Enter."""
58 return self.path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070059
Fang Deng26e4dc12018-03-04 19:01:59 -080060 def __exit__(self, exc_type, exc_value, traceback):
61 """Exit.
62
63 Args:
64 exc_type: Exception type raised within the context manager.
65 None if no execption is raised.
66 exc_value: Exception instance raised within the context manager.
67 None if no execption is raised.
68 traceback: Traceback for exeception that is raised within
69 the context manager.
70 None if no execption is raised.
71 Raises:
72 EnvironmentError or OSError when failed to delete temp directory.
73 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070074 try:
Fang Deng26e4dc12018-03-04 19:01:59 -080075 if self.path:
76 shutil.rmtree(self.path)
77 logger.debug("Deleted temporary dir %s", self.path)
78 except EnvironmentError as e:
79 # Ignore error if there is no exception raised
80 # within the with-clause and the EnvironementError is
81 # about problem that directory or file does not exist.
82 if not exc_type and e.errno != errno.ENOENT:
83 raise
84 except Exception as e: # pylint: disable=W0703
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070085 if exc_type:
Fang Deng26e4dc12018-03-04 19:01:59 -080086 logger.error(
cylan0d77ae12018-05-18 08:36:48 +000087 "Encountered error while deleting %s: %s",
88 self.path,
89 str(e),
90 exc_info=True)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070091 else:
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070092 raise
93
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070094
cylan0d77ae12018-05-18 08:36:48 +000095def RetryOnException(retry_checker,
96 max_retries,
97 sleep_multiplier=0,
Fang Dengf24be082018-02-10 10:09:55 -080098 retry_backoff_factor=1):
cylan0d77ae12018-05-18 08:36:48 +000099 """Decorater which retries the function call if |retry_checker| returns true.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700100
cylan0d77ae12018-05-18 08:36:48 +0000101 Args:
102 retry_checker: A callback function which should take an exception instance
103 and return True if functor(*args, **kwargs) should be retried
104 when such exception is raised, and return False if it should
105 not be retried.
106 max_retries: Maximum number of retries allowed.
107 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
108 retry_backoff_factor is 1. Will sleep
109 sleep_multiplier * (
110 retry_backoff_factor ** (attempt_count - 1))
111 if retry_backoff_factor != 1.
112 retry_backoff_factor: See explanation of sleep_multiplier.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700113
cylan0d77ae12018-05-18 08:36:48 +0000114 Returns:
115 The function wrapper.
116 """
117
118 def _Wrapper(func):
119 def _FunctionWrapper(*args, **kwargs):
120 return Retry(retry_checker, max_retries, func, sleep_multiplier,
121 retry_backoff_factor, *args, **kwargs)
122
123 return _FunctionWrapper
124
125 return _Wrapper
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700126
127
cylan4f73c1f2018-07-19 16:40:31 +0800128def Retry(retry_checker, max_retries, functor, sleep_multiplier,
129 retry_backoff_factor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000130 """Conditionally retry a function.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700131
cylan0d77ae12018-05-18 08:36:48 +0000132 Args:
133 retry_checker: A callback function which should take an exception instance
134 and return True if functor(*args, **kwargs) should be retried
135 when such exception is raised, and return False if it should
136 not be retried.
137 max_retries: Maximum number of retries allowed.
138 functor: The function to call, will call functor(*args, **kwargs).
139 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
140 retry_backoff_factor is 1. Will sleep
141 sleep_multiplier * (
142 retry_backoff_factor ** (attempt_count - 1))
143 if retry_backoff_factor != 1.
144 retry_backoff_factor: See explanation of sleep_multiplier.
145 *args: Arguments to pass to the functor.
146 **kwargs: Key-val based arguments to pass to the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700147
cylan0d77ae12018-05-18 08:36:48 +0000148 Returns:
149 The return value of the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700150
cylan0d77ae12018-05-18 08:36:48 +0000151 Raises:
152 Exception: The exception that functor(*args, **kwargs) throws.
153 """
154 attempt_count = 0
155 while attempt_count <= max_retries:
156 try:
157 attempt_count += 1
158 return_value = functor(*args, **kwargs)
159 return return_value
160 except Exception as e: # pylint: disable=W0703
161 if retry_checker(e) and attempt_count <= max_retries:
162 if retry_backoff_factor != 1:
163 sleep = sleep_multiplier * (retry_backoff_factor**
164 (attempt_count - 1))
165 else:
166 sleep = sleep_multiplier * attempt_count
Kevin Chengd25feee2018-05-24 10:15:20 -0700167 time.sleep(sleep)
cylan0d77ae12018-05-18 08:36:48 +0000168 else:
169 raise
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700170
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700171
Fang Dengf24be082018-02-10 10:09:55 -0800172def RetryExceptionType(exception_types, max_retries, functor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000173 """Retry exception if it is one of the given types.
Fang Dengf24be082018-02-10 10:09:55 -0800174
cylan0d77ae12018-05-18 08:36:48 +0000175 Args:
176 exception_types: A tuple of exception types, e.g. (ValueError, KeyError)
177 max_retries: Max number of retries allowed.
178 functor: The function to call. Will be retried if exception is raised and
179 the exception is one of the exception_types.
180 *args: Arguments to pass to Retry function.
181 **kwargs: Key-val based arguments to pass to Retry functions.
Fang Dengf24be082018-02-10 10:09:55 -0800182
cylan0d77ae12018-05-18 08:36:48 +0000183 Returns:
184 The value returned by calling functor.
185 """
186 return Retry(lambda e: isinstance(e, exception_types), max_retries,
187 functor, *args, **kwargs)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700188
189
190def PollAndWait(func, expected_return, timeout_exception, timeout_secs,
191 sleep_interval_secs, *args, **kwargs):
192 """Call a function until the function returns expected value or times out.
193
194 Args:
195 func: Function to call.
196 expected_return: The expected return value.
197 timeout_exception: Exception to raise when it hits timeout.
198 timeout_secs: Timeout seconds.
199 If 0 or less than zero, the function will run once and
200 we will not wait on it.
201 sleep_interval_secs: Time to sleep between two attemps.
202 *args: list of args to pass to func.
203 **kwargs: dictionary of keyword based args to pass to func.
204
205 Raises:
206 timeout_exception: if the run of function times out.
207 """
208 # TODO(fdeng): Currently this method does not kill
209 # |func|, if |func| takes longer than |timeout_secs|.
210 # We can use a more robust version from chromite.
211 start = time.time()
212 while True:
213 return_value = func(*args, **kwargs)
214 if return_value == expected_return:
215 return
216 elif time.time() - start > timeout_secs:
217 raise timeout_exception
218 else:
219 if sleep_interval_secs > 0:
220 time.sleep(sleep_interval_secs)
221
222
223def GenerateUniqueName(prefix=None, suffix=None):
Sam Chiu81bdc652018-06-29 18:45:08 +0800224 """Generate a random unique name using uuid4.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700225
226 Args:
227 prefix: String, desired prefix to prepend to the generated name.
228 suffix: String, desired suffix to append to the generated name.
229
230 Returns:
231 String, a random name.
232 """
233 name = uuid.uuid4().hex
234 if prefix:
235 name = "-".join([prefix, name])
236 if suffix:
237 name = "-".join([name, suffix])
238 return name
239
240
241def MakeTarFile(src_dict, dest):
242 """Archive files in tar.gz format to a file named as |dest|.
243
244 Args:
245 src_dict: A dictionary that maps a path to be archived
246 to the corresponding name that appears in the archive.
247 dest: String, path to output file, e.g. /tmp/myfile.tar.gz
248 """
249 logger.info("Compressing %s into %s.", src_dict.keys(), dest)
250 with tarfile.open(dest, "w:gz") as tar:
251 for src, arcname in src_dict.iteritems():
252 tar.add(src, arcname=arcname)
253
254
Fang Deng69498c32017-03-02 14:29:30 -0800255def CreateSshKeyPairIfNotExist(private_key_path, public_key_path):
256 """Create the ssh key pair if they don't exist.
257
cylan4f73c1f2018-07-19 16:40:31 +0800258 Case1. If the private key doesn't exist, we will create both the public key
259 and the private key.
260 Case2. If the private key exists but public key doesn't, we will create the
261 public key by using the private key.
262 Case3. If the public key exists but the private key doesn't, we will create
263 a new private key and overwrite the public key.
Fang Deng69498c32017-03-02 14:29:30 -0800264
265 Args:
266 private_key_path: Path to the private key file.
267 e.g. ~/.ssh/acloud_rsa
268 public_key_path: Path to the public key file.
269 e.g. ~/.ssh/acloud_rsa.pub
cylan4f73c1f2018-07-19 16:40:31 +0800270
Fang Deng69498c32017-03-02 14:29:30 -0800271 Raises:
272 error.DriverError: If failed to create the key pair.
273 """
274 public_key_path = os.path.expanduser(public_key_path)
275 private_key_path = os.path.expanduser(private_key_path)
cylan4f73c1f2018-07-19 16:40:31 +0800276 public_key_exist = os.path.exists(public_key_path)
277 private_key_exist = os.path.exists(private_key_path)
278 if public_key_exist and private_key_exist:
cylan0d77ae12018-05-18 08:36:48 +0000279 logger.debug(
cylan4f73c1f2018-07-19 16:40:31 +0800280 "The ssh private key (%s) and public key (%s) already exist,"
cylan0d77ae12018-05-18 08:36:48 +0000281 "will not automatically create the key pairs.", private_key_path,
282 public_key_path)
Fang Deng69498c32017-03-02 14:29:30 -0800283 return
cylan4f73c1f2018-07-19 16:40:31 +0800284 key_folder = os.path.dirname(private_key_path)
285 if not os.path.exists(key_folder):
286 os.makedirs(key_folder)
Fang Deng69498c32017-03-02 14:29:30 -0800287 try:
cylan4f73c1f2018-07-19 16:40:31 +0800288 if private_key_exist:
289 cmd = SSH_KEYGEN_PUB_CMD + ["-f", private_key_path]
290 with open(public_key_path, 'w') as outfile:
291 stream_content = subprocess.check_output(cmd)
292 outfile.write(
293 stream_content.rstrip('\n') + " " + getpass.getuser())
294 logger.info(
295 "The ssh public key (%s) do not exist, "
296 "automatically creating public key, calling: %s",
297 public_key_path, " ".join(cmd))
298 else:
299 cmd = SSH_KEYGEN_CMD + [
300 "-C", getpass.getuser(), "-f", private_key_path
301 ]
302 logger.info(
303 "Creating public key from private key (%s) via cmd: %s",
304 private_key_path, " ".join(cmd))
305 subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout)
Fang Deng69498c32017-03-02 14:29:30 -0800306 except subprocess.CalledProcessError as e:
cylan0d77ae12018-05-18 08:36:48 +0000307 raise errors.DriverError("Failed to create ssh key pair: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800308 except OSError as e:
309 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000310 "Failed to create ssh key pair, please make sure "
311 "'ssh-keygen' is installed: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800312
313 # By default ssh-keygen will create a public key file
314 # by append .pub to the private key file name. Rename it
315 # to what's requested by public_key_path.
316 default_pub_key_path = "%s.pub" % private_key_path
317 try:
318 if default_pub_key_path != public_key_path:
319 os.rename(default_pub_key_path, public_key_path)
320 except OSError as e:
321 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000322 "Failed to rename %s to %s: %s" % (default_pub_key_path,
323 public_key_path, str(e)))
Fang Deng69498c32017-03-02 14:29:30 -0800324
325 logger.info("Created ssh private key (%s) and public key (%s)",
326 private_key_path, public_key_path)
327
328
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700329def VerifyRsaPubKey(rsa):
330 """Verify the format of rsa public key.
331
332 Args:
333 rsa: content of rsa public key. It should follow the format of
334 ssh-rsa AAAAB3NzaC1yc2EA.... test@test.com
335
336 Raises:
337 DriverError if the format is not correct.
338 """
339 if not rsa or not all(ord(c) < 128 for c in rsa):
340 raise errors.DriverError(
341 "rsa key is empty or contains non-ascii character: %s" % rsa)
342
343 elements = rsa.split()
344 if len(elements) != 3:
345 raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa)
346
347 key_type, data, _ = elements
348 try:
349 binary_data = base64.decodestring(data)
350 # number of bytes of int type
351 int_length = 4
352 # binary_data is like "7ssh-key..." in a binary format.
353 # The first 4 bytes should represent 7, which should be
354 # the length of the following string "ssh-key".
355 # And the next 7 bytes should be string "ssh-key".
356 # We will verify that the rsa conforms to this format.
357 # ">I" in the following line means "big-endian unsigned integer".
358 type_length = struct.unpack(">I", binary_data[:int_length])[0]
359 if binary_data[int_length:int_length + type_length] != key_type:
360 raise errors.DriverError("rsa key is invalid: %s" % rsa)
361 except (struct.error, binascii.Error) as e:
cylan0d77ae12018-05-18 08:36:48 +0000362 raise errors.DriverError(
363 "rsa key is invalid: %s, error: %s" % (rsa, str(e)))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700364
365
Sam Chiu81bdc652018-06-29 18:45:08 +0800366# pylint: disable=old-style-class,no-init
367class TextColors:
368 """A class that defines common color ANSI code."""
369
370 HEADER = "\033[95m"
371 OKBLUE = "\033[94m"
372 OKGREEN = "\033[92m"
373 WARNING = "\033[93m"
374 FAIL = "\033[91m"
375 ENDC = "\033[0m"
376 BOLD = "\033[1m"
377 UNDERLINE = "\033[4m"
378
379
380def PrintColorString(message, colors=TextColors.OKBLUE):
381 """A helper function to print out colored text.
382
383 Args:
384 message: String, the message text.
385 colors: String, color code.
386 """
387 print(colors + message + TextColors.ENDC)
388
389
390def InteractWithQuestion(question, colors=TextColors.WARNING):
391 """A helper function to define the common way to run interactive cmd.
392
393 Args:
394 question: String, the question to ask user.
395 colors: String, color code.
396
397 Returns:
398 String, input from user.
399 """
400 return str(raw_input(colors + question + TextColors.ENDC).strip())
401
402
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700403class BatchHttpRequestExecutor(object):
404 """A helper class that executes requests in batch with retry.
405
406 This executor executes http requests in a batch and retry
407 those that have failed. It iteratively updates the dictionary
408 self._final_results with latest results, which can be retrieved
409 via GetResults.
410 """
411
412 def __init__(self,
413 execute_once_functor,
414 requests,
415 retry_http_codes=None,
416 max_retry=None,
417 sleep=None,
418 backoff_factor=None,
419 other_retriable_errors=None):
420 """Initializes the executor.
421
422 Args:
423 execute_once_functor: A function that execute requests in batch once.
424 It should return a dictionary like
425 {request_id: (response, exception)}
426 requests: A dictionary where key is request id picked by caller,
427 and value is a apiclient.http.HttpRequest.
428 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800429 max_retry: See utils.Retry.
430 sleep: See utils.Retry.
431 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700432 other_retriable_errors: A tuple of error types that should be retried
433 other than errors.HttpError.
434 """
435 self._execute_once_functor = execute_once_functor
436 self._requests = requests
437 # A dictionary that maps request id to pending request.
438 self._pending_requests = {}
439 # A dictionary that maps request id to a tuple (response, exception).
440 self._final_results = {}
441 self._retry_http_codes = retry_http_codes
442 self._max_retry = max_retry
443 self._sleep = sleep
444 self._backoff_factor = backoff_factor
445 self._other_retriable_errors = other_retriable_errors
446
447 def _ShoudRetry(self, exception):
Sam Chiu81bdc652018-06-29 18:45:08 +0800448 """Check if an exception is retriable.
449
450 Args:
451 exception: An exception instance.
452 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700453 if isinstance(exception, self._other_retriable_errors):
454 return True
455
cylan0d77ae12018-05-18 08:36:48 +0000456 if (isinstance(exception, errors.HttpError)
457 and exception.code in self._retry_http_codes):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700458 return True
459 return False
460
461 def _ExecuteOnce(self):
462 """Executes pending requests and update it with failed, retriable ones.
463
464 Raises:
465 HasRetriableRequestsError: if some requests fail and are retriable.
466 """
467 results = self._execute_once_functor(self._pending_requests)
468 # Update final_results with latest results.
469 self._final_results.update(results)
470 # Clear pending_requests
471 self._pending_requests.clear()
472 for request_id, result in results.iteritems():
473 exception = result[1]
474 if exception is not None and self._ShoudRetry(exception):
475 # If this is a retriable exception, put it in pending_requests
476 self._pending_requests[request_id] = self._requests[request_id]
477 if self._pending_requests:
478 # If there is still retriable requests pending, raise an error
Fang Dengf24be082018-02-10 10:09:55 -0800479 # so that Retry will retry this function with pending_requests.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700480 raise errors.HasRetriableRequestsError(
cylan0d77ae12018-05-18 08:36:48 +0000481 "Retriable errors: %s" %
482 [str(results[rid][1]) for rid in self._pending_requests])
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700483
484 def Execute(self):
485 """Executes the requests and retry if necessary.
486
487 Will populate self._final_results.
488 """
cylan0d77ae12018-05-18 08:36:48 +0000489
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700490 def _ShouldRetryHandler(exc):
491 """Check if |exc| is a retriable exception.
492
493 Args:
494 exc: An exception.
495
496 Returns:
497 True if exception is of type HasRetriableRequestsError; False otherwise.
498 """
499 should_retry = isinstance(exc, errors.HasRetriableRequestsError)
500 if should_retry:
501 logger.info("Will retry failed requests.", exc_info=True)
502 logger.info("%s", exc)
503 return should_retry
504
505 try:
506 self._pending_requests = self._requests.copy()
Fang Dengf24be082018-02-10 10:09:55 -0800507 Retry(
cylan0d77ae12018-05-18 08:36:48 +0000508 _ShouldRetryHandler,
509 max_retries=self._max_retry,
Fang Dengf24be082018-02-10 10:09:55 -0800510 functor=self._ExecuteOnce,
511 sleep_multiplier=self._sleep,
512 retry_backoff_factor=self._backoff_factor)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700513 except errors.HasRetriableRequestsError:
514 logger.debug("Some requests did not succeed after retry.")
515
516 def GetResults(self):
517 """Returns final results.
518
519 Returns:
520 results, a dictionary in the following format
521 {request_id: (response, exception)}
522 request_ids are those from requests; response
523 is the http response for the request or None on error;
524 exception is an instance of DriverError or None if no error.
525 """
526 return self._final_results