blob: 35729a637bc238353dbca9e90cf7f32f01995a11 [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
18import base64
19import binascii
20import errno
Fang Deng69498c32017-03-02 14:29:30 -080021import getpass
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070022import logging
23import os
24import shutil
25import struct
Fang Deng69498c32017-03-02 14:29:30 -080026import subprocess
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070027import sys
28import tarfile
29import tempfile
30import time
31import uuid
32
33from acloud.public import errors
34
35logger = logging.getLogger(__name__)
36
Fang Deng69498c32017-03-02 14:29:30 -080037SSH_KEYGEN_CMD = ["ssh-keygen", "-t", "rsa", "-b", "4096"]
Kevin Chengd25feee2018-05-24 10:15:20 -070038DEFAULT_RETRY_BACKOFF_FACTOR = 1
39DEFAULT_SLEEP_MULTIPLIER = 0
Fang Deng69498c32017-03-02 14:29:30 -080040
41
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070042class TempDir(object):
Fang Deng26e4dc12018-03-04 19:01:59 -080043 """A context manager that ceates a temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070044
Fang Deng26e4dc12018-03-04 19:01:59 -080045 Attributes:
46 path: The path of the temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070047 """
48
Fang Deng26e4dc12018-03-04 19:01:59 -080049 def __init__(self):
50 self.path = tempfile.mkdtemp()
51 os.chmod(self.path, 0o700)
52 logger.debug("Created temporary dir %s", self.path)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070053
54 def __enter__(self):
Fang Deng26e4dc12018-03-04 19:01:59 -080055 """Enter."""
56 return self.path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070057
Fang Deng26e4dc12018-03-04 19:01:59 -080058 def __exit__(self, exc_type, exc_value, traceback):
59 """Exit.
60
61 Args:
62 exc_type: Exception type raised within the context manager.
63 None if no execption is raised.
64 exc_value: Exception instance raised within the context manager.
65 None if no execption is raised.
66 traceback: Traceback for exeception that is raised within
67 the context manager.
68 None if no execption is raised.
69 Raises:
70 EnvironmentError or OSError when failed to delete temp directory.
71 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070072 try:
Fang Deng26e4dc12018-03-04 19:01:59 -080073 if self.path:
74 shutil.rmtree(self.path)
75 logger.debug("Deleted temporary dir %s", self.path)
76 except EnvironmentError as e:
77 # Ignore error if there is no exception raised
78 # within the with-clause and the EnvironementError is
79 # about problem that directory or file does not exist.
80 if not exc_type and e.errno != errno.ENOENT:
81 raise
82 except Exception as e: # pylint: disable=W0703
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070083 if exc_type:
Fang Deng26e4dc12018-03-04 19:01:59 -080084 logger.error(
cylan0d77ae12018-05-18 08:36:48 +000085 "Encountered error while deleting %s: %s",
86 self.path,
87 str(e),
88 exc_info=True)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070089 else:
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070090 raise
91
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070092
cylan0d77ae12018-05-18 08:36:48 +000093def RetryOnException(retry_checker,
94 max_retries,
95 sleep_multiplier=0,
Fang Dengf24be082018-02-10 10:09:55 -080096 retry_backoff_factor=1):
cylan0d77ae12018-05-18 08:36:48 +000097 """Decorater which retries the function call if |retry_checker| returns true.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070098
cylan0d77ae12018-05-18 08:36:48 +000099 Args:
100 retry_checker: A callback function which should take an exception instance
101 and return True if functor(*args, **kwargs) should be retried
102 when such exception is raised, and return False if it should
103 not be retried.
104 max_retries: Maximum number of retries allowed.
105 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
106 retry_backoff_factor is 1. Will sleep
107 sleep_multiplier * (
108 retry_backoff_factor ** (attempt_count - 1))
109 if retry_backoff_factor != 1.
110 retry_backoff_factor: See explanation of sleep_multiplier.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700111
cylan0d77ae12018-05-18 08:36:48 +0000112 Returns:
113 The function wrapper.
114 """
115
116 def _Wrapper(func):
117 def _FunctionWrapper(*args, **kwargs):
118 return Retry(retry_checker, max_retries, func, sleep_multiplier,
119 retry_backoff_factor, *args, **kwargs)
120
121 return _FunctionWrapper
122
123 return _Wrapper
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700124
125
cylan0d77ae12018-05-18 08:36:48 +0000126def Retry(retry_checker,
127 max_retries,
128 functor,
129 sleep_multiplier,
130 retry_backoff_factor,
131 *args,
132 **kwargs):
133 """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):
227 """Generate a random unque name using uuid4.
228
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
261 Check if the public and private key pairs exist at
262 the given places. If not, create them.
263
264 Args:
265 private_key_path: Path to the private key file.
266 e.g. ~/.ssh/acloud_rsa
267 public_key_path: Path to the public key file.
268 e.g. ~/.ssh/acloud_rsa.pub
269 Raises:
270 error.DriverError: If failed to create the key pair.
271 """
272 public_key_path = os.path.expanduser(public_key_path)
273 private_key_path = os.path.expanduser(private_key_path)
cylan0d77ae12018-05-18 08:36:48 +0000274 create_key = (not os.path.exists(public_key_path)
275 and not os.path.exists(private_key_path))
Fang Deng69498c32017-03-02 14:29:30 -0800276 if not create_key:
cylan0d77ae12018-05-18 08:36:48 +0000277 logger.debug(
278 "The ssh private key (%s) or public key (%s) already exist,"
279 "will not automatically create the key pairs.", private_key_path,
280 public_key_path)
Fang Deng69498c32017-03-02 14:29:30 -0800281 return
282 cmd = SSH_KEYGEN_CMD + ["-C", getpass.getuser(), "-f", private_key_path]
cylan0d77ae12018-05-18 08:36:48 +0000283 logger.info(
284 "The ssh private key (%s) and public key (%s) do not exist, "
285 "automatically creating key pair, calling: %s", private_key_path,
286 public_key_path, " ".join(cmd))
Fang Deng69498c32017-03-02 14:29:30 -0800287 try:
288 subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout)
289 except subprocess.CalledProcessError as e:
cylan0d77ae12018-05-18 08:36:48 +0000290 raise errors.DriverError("Failed to create ssh key pair: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800291 except OSError as e:
292 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000293 "Failed to create ssh key pair, please make sure "
294 "'ssh-keygen' is installed: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800295
296 # By default ssh-keygen will create a public key file
297 # by append .pub to the private key file name. Rename it
298 # to what's requested by public_key_path.
299 default_pub_key_path = "%s.pub" % private_key_path
300 try:
301 if default_pub_key_path != public_key_path:
302 os.rename(default_pub_key_path, public_key_path)
303 except OSError as e:
304 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000305 "Failed to rename %s to %s: %s" % (default_pub_key_path,
306 public_key_path, str(e)))
Fang Deng69498c32017-03-02 14:29:30 -0800307
308 logger.info("Created ssh private key (%s) and public key (%s)",
309 private_key_path, public_key_path)
310
311
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700312def VerifyRsaPubKey(rsa):
313 """Verify the format of rsa public key.
314
315 Args:
316 rsa: content of rsa public key. It should follow the format of
317 ssh-rsa AAAAB3NzaC1yc2EA.... test@test.com
318
319 Raises:
320 DriverError if the format is not correct.
321 """
322 if not rsa or not all(ord(c) < 128 for c in rsa):
323 raise errors.DriverError(
324 "rsa key is empty or contains non-ascii character: %s" % rsa)
325
326 elements = rsa.split()
327 if len(elements) != 3:
328 raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa)
329
330 key_type, data, _ = elements
331 try:
332 binary_data = base64.decodestring(data)
333 # number of bytes of int type
334 int_length = 4
335 # binary_data is like "7ssh-key..." in a binary format.
336 # The first 4 bytes should represent 7, which should be
337 # the length of the following string "ssh-key".
338 # And the next 7 bytes should be string "ssh-key".
339 # We will verify that the rsa conforms to this format.
340 # ">I" in the following line means "big-endian unsigned integer".
341 type_length = struct.unpack(">I", binary_data[:int_length])[0]
342 if binary_data[int_length:int_length + type_length] != key_type:
343 raise errors.DriverError("rsa key is invalid: %s" % rsa)
344 except (struct.error, binascii.Error) as e:
cylan0d77ae12018-05-18 08:36:48 +0000345 raise errors.DriverError(
346 "rsa key is invalid: %s, error: %s" % (rsa, str(e)))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700347
348
349class BatchHttpRequestExecutor(object):
350 """A helper class that executes requests in batch with retry.
351
352 This executor executes http requests in a batch and retry
353 those that have failed. It iteratively updates the dictionary
354 self._final_results with latest results, which can be retrieved
355 via GetResults.
356 """
357
358 def __init__(self,
359 execute_once_functor,
360 requests,
361 retry_http_codes=None,
362 max_retry=None,
363 sleep=None,
364 backoff_factor=None,
365 other_retriable_errors=None):
366 """Initializes the executor.
367
368 Args:
369 execute_once_functor: A function that execute requests in batch once.
370 It should return a dictionary like
371 {request_id: (response, exception)}
372 requests: A dictionary where key is request id picked by caller,
373 and value is a apiclient.http.HttpRequest.
374 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800375 max_retry: See utils.Retry.
376 sleep: See utils.Retry.
377 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700378 other_retriable_errors: A tuple of error types that should be retried
379 other than errors.HttpError.
380 """
381 self._execute_once_functor = execute_once_functor
382 self._requests = requests
383 # A dictionary that maps request id to pending request.
384 self._pending_requests = {}
385 # A dictionary that maps request id to a tuple (response, exception).
386 self._final_results = {}
387 self._retry_http_codes = retry_http_codes
388 self._max_retry = max_retry
389 self._sleep = sleep
390 self._backoff_factor = backoff_factor
391 self._other_retriable_errors = other_retriable_errors
392
393 def _ShoudRetry(self, exception):
394 """Check if an exception is retriable."""
395 if isinstance(exception, self._other_retriable_errors):
396 return True
397
cylan0d77ae12018-05-18 08:36:48 +0000398 if (isinstance(exception, errors.HttpError)
399 and exception.code in self._retry_http_codes):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700400 return True
401 return False
402
403 def _ExecuteOnce(self):
404 """Executes pending requests and update it with failed, retriable ones.
405
406 Raises:
407 HasRetriableRequestsError: if some requests fail and are retriable.
408 """
409 results = self._execute_once_functor(self._pending_requests)
410 # Update final_results with latest results.
411 self._final_results.update(results)
412 # Clear pending_requests
413 self._pending_requests.clear()
414 for request_id, result in results.iteritems():
415 exception = result[1]
416 if exception is not None and self._ShoudRetry(exception):
417 # If this is a retriable exception, put it in pending_requests
418 self._pending_requests[request_id] = self._requests[request_id]
419 if self._pending_requests:
420 # If there is still retriable requests pending, raise an error
Fang Dengf24be082018-02-10 10:09:55 -0800421 # so that Retry will retry this function with pending_requests.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700422 raise errors.HasRetriableRequestsError(
cylan0d77ae12018-05-18 08:36:48 +0000423 "Retriable errors: %s" %
424 [str(results[rid][1]) for rid in self._pending_requests])
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700425
426 def Execute(self):
427 """Executes the requests and retry if necessary.
428
429 Will populate self._final_results.
430 """
cylan0d77ae12018-05-18 08:36:48 +0000431
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700432 def _ShouldRetryHandler(exc):
433 """Check if |exc| is a retriable exception.
434
435 Args:
436 exc: An exception.
437
438 Returns:
439 True if exception is of type HasRetriableRequestsError; False otherwise.
440 """
441 should_retry = isinstance(exc, errors.HasRetriableRequestsError)
442 if should_retry:
443 logger.info("Will retry failed requests.", exc_info=True)
444 logger.info("%s", exc)
445 return should_retry
446
447 try:
448 self._pending_requests = self._requests.copy()
Fang Dengf24be082018-02-10 10:09:55 -0800449 Retry(
cylan0d77ae12018-05-18 08:36:48 +0000450 _ShouldRetryHandler,
451 max_retries=self._max_retry,
Fang Dengf24be082018-02-10 10:09:55 -0800452 functor=self._ExecuteOnce,
453 sleep_multiplier=self._sleep,
454 retry_backoff_factor=self._backoff_factor)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700455 except errors.HasRetriableRequestsError:
456 logger.debug("Some requests did not succeed after retry.")
457
458 def GetResults(self):
459 """Returns final results.
460
461 Returns:
462 results, a dictionary in the following format
463 {request_id: (response, exception)}
464 request_ids are those from requests; response
465 is the http response for the request or None on error;
466 exception is an instance of DriverError or None if no error.
467 """
468 return self._final_results