blob: 54524dacc1833f09c2acc8d0dc0065b90c96a7cf [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.
16
17"""Common Utilities.
18
19The following code is copied from chromite with modifications.
20 - class TempDir: chromite/lib/osutils.py
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070021
22"""
23
24import base64
25import binascii
26import errno
Fang Deng69498c32017-03-02 14:29:30 -080027import getpass
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070028import logging
29import os
30import shutil
31import struct
Fang Deng69498c32017-03-02 14:29:30 -080032import subprocess
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070033import sys
34import tarfile
35import tempfile
36import time
37import uuid
38
39from acloud.public import errors
40
Fang Deng69498c32017-03-02 14:29:30 -080041
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070042logger = logging.getLogger(__name__)
43
44
Fang Deng69498c32017-03-02 14:29:30 -080045SSH_KEYGEN_CMD = ["ssh-keygen", "-t", "rsa", "-b", "4096"]
46
47
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070048class TempDir(object):
49 """Object that creates a temporary directory.
50
51 This object can either be used as a context manager or just as a simple
52 object. The temporary directory is stored as self.tempdir in the object, and
53 is returned as a string by a 'with' statement.
54 """
55
56 def __init__(self, prefix='tmp', base_dir=None, delete=True):
57 """Constructor. Creates the temporary directory.
58
59 Args:
60 prefix: See tempfile.mkdtemp documentation.
61 base_dir: The directory to place the temporary directory.
62 If None, will choose from system default tmp dir.
63 delete: Whether the temporary dir should be deleted as part of cleanup.
64 """
65 self.delete = delete
66 self.tempdir = tempfile.mkdtemp(prefix=prefix, dir=base_dir)
67 os.chmod(self.tempdir, 0o700)
68
69 def Cleanup(self):
70 """Clean up the temporary directory."""
71 # Note that _TempDirSetup may have failed, resulting in these attributes
72 # not being set; this is why we use getattr here (and must).
73 tempdir = getattr(self, 'tempdir', None)
74 if tempdir is not None and self.delete:
75 try:
76 shutil.rmtree(tempdir)
77 except EnvironmentError as e:
78 # Ignore error if directory or file does not exist.
79 if e.errno != errno.ENOENT:
80 raise
81 finally:
82 self.tempdir = None
83
84 def __enter__(self):
85 """Return the temporary directory."""
86 return self.tempdir
87
88 def __exit__(self, exc_type, exc_value, exc_traceback):
89 """Exit the context manager."""
90 try:
91 self.Cleanup()
92 except Exception: # pylint: disable=W0703
93 if exc_type:
94 # If an exception from inside the context was already in progress,
95 # log our cleanup exception, then allow the original to resume.
96 logger.error('While exiting %s:', self, exc_info=True)
97
98 if self.tempdir:
99 # Log all files in tempdir at the time of the failure.
100 try:
101 logger.error('Directory contents were:')
102 for name in os.listdir(self.tempdir):
103 logger.error(' %s', name)
104 except OSError:
105 logger.error(' Directory did not exist.')
106 else:
107 # If there was not an exception from the context, raise ours.
108 raise
109
110 def __del__(self):
111 """Delete the object."""
112 self.Cleanup()
113
Fang Dengf24be082018-02-10 10:09:55 -0800114def RetryOnException(retry_checker, max_retries, sleep_multiplier=0,
115 retry_backoff_factor=1):
116 """Decorater which retries the function call if |retry_checker| returns true.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700117
Fang Dengf24be082018-02-10 10:09:55 -0800118 Args:
119 retry_checker: A callback function which should take an exception instance
120 and return True if functor(*args, **kwargs) should be retried
121 when such exception is raised, and return False if it should
122 not be retried.
123 max_retries: Maximum number of retries allowed.
124 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
125 retry_backoff_factor is 1. Will sleep
126 sleep_multiplier * (
127 retry_backoff_factor ** (attempt_count - 1))
128 if retry_backoff_factor != 1.
129 retry_backoff_factor: See explanation of sleep_multiplier.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700130
Fang Dengf24be082018-02-10 10:09:55 -0800131 Returns:
132 The function wrapper.
133 """
134 def _Wrapper(func):
135 def _FunctionWrapper(*args, **kwargs):
136 return Retry(retry_checker, max_retries, func, sleep_multiplier,
137 retry_backoff_factor,
138 *args, **kwargs)
139 return _FunctionWrapper
140 return _Wrapper
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700141
142
Fang Dengf24be082018-02-10 10:09:55 -0800143def Retry(retry_checker, max_retries, functor, sleep_multiplier=0,
144 retry_backoff_factor=1, *args, **kwargs):
145 """Conditionally retry a function.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700146
Fang Dengf24be082018-02-10 10:09:55 -0800147 Args:
148 retry_checker: A callback function which should take an exception instance
149 and return True if functor(*args, **kwargs) should be retried
150 when such exception is raised, and return False if it should
151 not be retried.
152 max_retries: Maximum number of retries allowed.
153 functor: The function to call, will call functor(*args, **kwargs).
154 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
155 retry_backoff_factor is 1. Will sleep
156 sleep_multiplier * (
157 retry_backoff_factor ** (attempt_count - 1))
158 if retry_backoff_factor != 1.
159 retry_backoff_factor: See explanation of sleep_multiplier.
160 *args: Arguments to pass to the functor.
161 **kwargs: Key-val based arguments to pass to the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700162
Fang Dengf24be082018-02-10 10:09:55 -0800163 Returns:
164 The return value of the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700165
Fang Dengf24be082018-02-10 10:09:55 -0800166 Raises:
167 Exception: The exception that functor(*args, **kwargs) throws.
168 """
169 attempt_count = 0
170 while attempt_count <= max_retries:
171 try:
172 attempt_count += 1
173 return_value = functor(*args, **kwargs)
174 return return_value
175 except Exception as e: # pylint: disable=W0703
176 if retry_checker(e) and attempt_count <= max_retries:
177 if retry_backoff_factor != 1:
178 sleep = sleep_multiplier * (
179 retry_backoff_factor ** (attempt_count - 1))
180 else:
181 sleep = sleep_multiplier * attempt_count
182 time.sleep(sleep)
183 else:
184 raise
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700185
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700186
Fang Dengf24be082018-02-10 10:09:55 -0800187def RetryExceptionType(exception_types, max_retries, functor, *args, **kwargs):
188 """Retry exception if it is one of the given types.
189
190 Args:
191 exception_types: A tuple of exception types, e.g. (ValueError, KeyError)
192 max_retries: Max number of retries allowed.
193 functor: The function to call. Will be retried if exception is raised and
194 the exception is one of the exception_types.
195 *args: Arguments to pass to Retry function.
196 **kwargs: Key-val based arguments to pass to Retry functions.
197
198 Returns:
199 The value returned by calling functor.
200 """
201 return Retry(lambda e: isinstance(e, exception_types), max_retries,
202 functor, *args, **kwargs)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700203
204
205def PollAndWait(func, expected_return, timeout_exception, timeout_secs,
206 sleep_interval_secs, *args, **kwargs):
207 """Call a function until the function returns expected value or times out.
208
209 Args:
210 func: Function to call.
211 expected_return: The expected return value.
212 timeout_exception: Exception to raise when it hits timeout.
213 timeout_secs: Timeout seconds.
214 If 0 or less than zero, the function will run once and
215 we will not wait on it.
216 sleep_interval_secs: Time to sleep between two attemps.
217 *args: list of args to pass to func.
218 **kwargs: dictionary of keyword based args to pass to func.
219
220 Raises:
221 timeout_exception: if the run of function times out.
222 """
223 # TODO(fdeng): Currently this method does not kill
224 # |func|, if |func| takes longer than |timeout_secs|.
225 # We can use a more robust version from chromite.
226 start = time.time()
227 while True:
228 return_value = func(*args, **kwargs)
229 if return_value == expected_return:
230 return
231 elif time.time() - start > timeout_secs:
232 raise timeout_exception
233 else:
234 if sleep_interval_secs > 0:
235 time.sleep(sleep_interval_secs)
236
237
238def GenerateUniqueName(prefix=None, suffix=None):
239 """Generate a random unque name using uuid4.
240
241 Args:
242 prefix: String, desired prefix to prepend to the generated name.
243 suffix: String, desired suffix to append to the generated name.
244
245 Returns:
246 String, a random name.
247 """
248 name = uuid.uuid4().hex
249 if prefix:
250 name = "-".join([prefix, name])
251 if suffix:
252 name = "-".join([name, suffix])
253 return name
254
255
256def MakeTarFile(src_dict, dest):
257 """Archive files in tar.gz format to a file named as |dest|.
258
259 Args:
260 src_dict: A dictionary that maps a path to be archived
261 to the corresponding name that appears in the archive.
262 dest: String, path to output file, e.g. /tmp/myfile.tar.gz
263 """
264 logger.info("Compressing %s into %s.", src_dict.keys(), dest)
265 with tarfile.open(dest, "w:gz") as tar:
266 for src, arcname in src_dict.iteritems():
267 tar.add(src, arcname=arcname)
268
269
Fang Deng69498c32017-03-02 14:29:30 -0800270def CreateSshKeyPairIfNotExist(private_key_path, public_key_path):
271 """Create the ssh key pair if they don't exist.
272
273 Check if the public and private key pairs exist at
274 the given places. If not, create them.
275
276 Args:
277 private_key_path: Path to the private key file.
278 e.g. ~/.ssh/acloud_rsa
279 public_key_path: Path to the public key file.
280 e.g. ~/.ssh/acloud_rsa.pub
281 Raises:
282 error.DriverError: If failed to create the key pair.
283 """
284 public_key_path = os.path.expanduser(public_key_path)
285 private_key_path = os.path.expanduser(private_key_path)
286 create_key = (
287 not os.path.exists(public_key_path) and
288 not os.path.exists(private_key_path))
289 if not create_key:
290 logger.debug("The ssh private key (%s) or public key (%s) already exist,"
291 "will not automatically create the key pairs.",
292 private_key_path, public_key_path)
293 return
294 cmd = SSH_KEYGEN_CMD + ["-C", getpass.getuser(), "-f", private_key_path]
295 logger.info("The ssh private key (%s) and public key (%s) do not exist, "
296 "automatically creating key pair, calling: %s",
297 private_key_path, public_key_path, " ".join(cmd))
298 try:
299 subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout)
300 except subprocess.CalledProcessError as e:
301 raise errors.DriverError(
302 "Failed to create ssh key pair: %s" % str(e))
303 except OSError as e:
304 raise errors.DriverError(
305 "Failed to create ssh key pair, please make sure "
306 "'ssh-keygen' is installed: %s" % str(e))
307
308 # By default ssh-keygen will create a public key file
309 # by append .pub to the private key file name. Rename it
310 # to what's requested by public_key_path.
311 default_pub_key_path = "%s.pub" % private_key_path
312 try:
313 if default_pub_key_path != public_key_path:
314 os.rename(default_pub_key_path, public_key_path)
315 except OSError as e:
316 raise errors.DriverError(
317 "Failed to rename %s to %s: %s" %
318 (default_pub_key_path, public_key_path, str(e)))
319
320 logger.info("Created ssh private key (%s) and public key (%s)",
321 private_key_path, public_key_path)
322
323
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700324def VerifyRsaPubKey(rsa):
325 """Verify the format of rsa public key.
326
327 Args:
328 rsa: content of rsa public key. It should follow the format of
329 ssh-rsa AAAAB3NzaC1yc2EA.... test@test.com
330
331 Raises:
332 DriverError if the format is not correct.
333 """
334 if not rsa or not all(ord(c) < 128 for c in rsa):
335 raise errors.DriverError(
336 "rsa key is empty or contains non-ascii character: %s" % rsa)
337
338 elements = rsa.split()
339 if len(elements) != 3:
340 raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa)
341
342 key_type, data, _ = elements
343 try:
344 binary_data = base64.decodestring(data)
345 # number of bytes of int type
346 int_length = 4
347 # binary_data is like "7ssh-key..." in a binary format.
348 # The first 4 bytes should represent 7, which should be
349 # the length of the following string "ssh-key".
350 # And the next 7 bytes should be string "ssh-key".
351 # We will verify that the rsa conforms to this format.
352 # ">I" in the following line means "big-endian unsigned integer".
353 type_length = struct.unpack(">I", binary_data[:int_length])[0]
354 if binary_data[int_length:int_length + type_length] != key_type:
355 raise errors.DriverError("rsa key is invalid: %s" % rsa)
356 except (struct.error, binascii.Error) as e:
357 raise errors.DriverError("rsa key is invalid: %s, error: %s" %
358 (rsa, str(e)))
359
360
361class BatchHttpRequestExecutor(object):
362 """A helper class that executes requests in batch with retry.
363
364 This executor executes http requests in a batch and retry
365 those that have failed. It iteratively updates the dictionary
366 self._final_results with latest results, which can be retrieved
367 via GetResults.
368 """
369
370 def __init__(self,
371 execute_once_functor,
372 requests,
373 retry_http_codes=None,
374 max_retry=None,
375 sleep=None,
376 backoff_factor=None,
377 other_retriable_errors=None):
378 """Initializes the executor.
379
380 Args:
381 execute_once_functor: A function that execute requests in batch once.
382 It should return a dictionary like
383 {request_id: (response, exception)}
384 requests: A dictionary where key is request id picked by caller,
385 and value is a apiclient.http.HttpRequest.
386 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800387 max_retry: See utils.Retry.
388 sleep: See utils.Retry.
389 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700390 other_retriable_errors: A tuple of error types that should be retried
391 other than errors.HttpError.
392 """
393 self._execute_once_functor = execute_once_functor
394 self._requests = requests
395 # A dictionary that maps request id to pending request.
396 self._pending_requests = {}
397 # A dictionary that maps request id to a tuple (response, exception).
398 self._final_results = {}
399 self._retry_http_codes = retry_http_codes
400 self._max_retry = max_retry
401 self._sleep = sleep
402 self._backoff_factor = backoff_factor
403 self._other_retriable_errors = other_retriable_errors
404
405 def _ShoudRetry(self, exception):
406 """Check if an exception is retriable."""
407 if isinstance(exception, self._other_retriable_errors):
408 return True
409
410 if (isinstance(exception, errors.HttpError) and
411 exception.code in self._retry_http_codes):
412 return True
413 return False
414
415 def _ExecuteOnce(self):
416 """Executes pending requests and update it with failed, retriable ones.
417
418 Raises:
419 HasRetriableRequestsError: if some requests fail and are retriable.
420 """
421 results = self._execute_once_functor(self._pending_requests)
422 # Update final_results with latest results.
423 self._final_results.update(results)
424 # Clear pending_requests
425 self._pending_requests.clear()
426 for request_id, result in results.iteritems():
427 exception = result[1]
428 if exception is not None and self._ShoudRetry(exception):
429 # If this is a retriable exception, put it in pending_requests
430 self._pending_requests[request_id] = self._requests[request_id]
431 if self._pending_requests:
432 # If there is still retriable requests pending, raise an error
Fang Dengf24be082018-02-10 10:09:55 -0800433 # so that Retry will retry this function with pending_requests.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700434 raise errors.HasRetriableRequestsError(
435 "Retriable errors: %s" % [str(results[rid][1])
436 for rid in self._pending_requests])
437
438 def Execute(self):
439 """Executes the requests and retry if necessary.
440
441 Will populate self._final_results.
442 """
443 def _ShouldRetryHandler(exc):
444 """Check if |exc| is a retriable exception.
445
446 Args:
447 exc: An exception.
448
449 Returns:
450 True if exception is of type HasRetriableRequestsError; False otherwise.
451 """
452 should_retry = isinstance(exc, errors.HasRetriableRequestsError)
453 if should_retry:
454 logger.info("Will retry failed requests.", exc_info=True)
455 logger.info("%s", exc)
456 return should_retry
457
458 try:
459 self._pending_requests = self._requests.copy()
Fang Dengf24be082018-02-10 10:09:55 -0800460 Retry(
461 _ShouldRetryHandler, max_retries=self._max_retry,
462 functor=self._ExecuteOnce,
463 sleep_multiplier=self._sleep,
464 retry_backoff_factor=self._backoff_factor)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700465 except errors.HasRetriableRequestsError:
466 logger.debug("Some requests did not succeed after retry.")
467
468 def GetResults(self):
469 """Returns final results.
470
471 Returns:
472 results, a dictionary in the following format
473 {request_id: (response, exception)}
474 request_ids are those from requests; response
475 is the http response for the request or None on error;
476 exception is an instance of DriverError or None if no error.
477 """
478 return self._final_results