blob: 4acacbc9ef52f90473cb0c1f386f3d46a76a46c3 [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
cylan66713722018-10-06 01:38:26 +080019
20from distutils.spawn import find_executable
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070021import base64
22import binascii
cylan66713722018-10-06 01:38:26 +080023import collections
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070024import errno
Fang Deng69498c32017-03-02 14:29:30 -080025import getpass
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070026import logging
27import os
28import shutil
29import struct
cylan66713722018-10-06 01:38:26 +080030import socket
Fang Deng69498c32017-03-02 14:29:30 -080031import subprocess
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070032import sys
33import tarfile
34import tempfile
35import time
36import uuid
chojoycecd004bc2018-09-13 10:39:00 +080037import zipfile
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070038
chojoycecd004bc2018-09-13 10:39:00 +080039from acloud import errors as root_errors
herbertxue34776bb2018-07-03 21:57:48 +080040from acloud.internal import constants
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070041from acloud.public import errors
42
43logger = logging.getLogger(__name__)
44
Fang Deng69498c32017-03-02 14:29:30 -080045SSH_KEYGEN_CMD = ["ssh-keygen", "-t", "rsa", "-b", "4096"]
cylan4f73c1f2018-07-19 16:40:31 +080046SSH_KEYGEN_PUB_CMD = ["ssh-keygen", "-y"]
Kevin Chengd25feee2018-05-24 10:15:20 -070047DEFAULT_RETRY_BACKOFF_FACTOR = 1
48DEFAULT_SLEEP_MULTIPLIER = 0
Fang Deng69498c32017-03-02 14:29:30 -080049
cylan66713722018-10-06 01:38:26 +080050SSH_BIN = "ssh"
51_SSH_TUNNEL_ARGS = ("-i %(rsa_key_file)s -o UserKnownHostsFile=/dev/null "
52 "-o StrictHostKeyChecking=no "
53 "-L %(vnc_port)d:127.0.0.1:%(target_vnc_port)d "
54 "-L %(adb_port)d:127.0.0.1:%(target_adb_port)d "
55 "-N -f -l %(ssh_user)s %(ip_addr)s")
56ADB_BIN = "adb"
57_ADB_CONNECT_ARGS = "connect 127.0.0.1:%(adb_port)d"
58# Store the ports that vnc/adb are forwarded to, both are integers.
59ForwardedPorts = collections.namedtuple("ForwardedPorts", [constants.VNC_PORT,
60 constants.ADB_PORT])
Fang Deng69498c32017-03-02 14:29:30 -080061
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070062class TempDir(object):
Fang Deng26e4dc12018-03-04 19:01:59 -080063 """A context manager that ceates a temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070064
Fang Deng26e4dc12018-03-04 19:01:59 -080065 Attributes:
Sam Chiu81bdc652018-06-29 18:45:08 +080066 path: The path of the temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070067 """
68
Fang Deng26e4dc12018-03-04 19:01:59 -080069 def __init__(self):
70 self.path = tempfile.mkdtemp()
71 os.chmod(self.path, 0o700)
72 logger.debug("Created temporary dir %s", self.path)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070073
74 def __enter__(self):
Fang Deng26e4dc12018-03-04 19:01:59 -080075 """Enter."""
76 return self.path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070077
Fang Deng26e4dc12018-03-04 19:01:59 -080078 def __exit__(self, exc_type, exc_value, traceback):
79 """Exit.
80
81 Args:
82 exc_type: Exception type raised within the context manager.
83 None if no execption is raised.
84 exc_value: Exception instance raised within the context manager.
85 None if no execption is raised.
86 traceback: Traceback for exeception that is raised within
87 the context manager.
88 None if no execption is raised.
89 Raises:
90 EnvironmentError or OSError when failed to delete temp directory.
91 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070092 try:
Fang Deng26e4dc12018-03-04 19:01:59 -080093 if self.path:
94 shutil.rmtree(self.path)
95 logger.debug("Deleted temporary dir %s", self.path)
96 except EnvironmentError as e:
97 # Ignore error if there is no exception raised
98 # within the with-clause and the EnvironementError is
99 # about problem that directory or file does not exist.
100 if not exc_type and e.errno != errno.ENOENT:
101 raise
102 except Exception as e: # pylint: disable=W0703
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700103 if exc_type:
Fang Deng26e4dc12018-03-04 19:01:59 -0800104 logger.error(
cylan0d77ae12018-05-18 08:36:48 +0000105 "Encountered error while deleting %s: %s",
106 self.path,
107 str(e),
108 exc_info=True)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700109 else:
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700110 raise
111
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700112
cylan0d77ae12018-05-18 08:36:48 +0000113def RetryOnException(retry_checker,
114 max_retries,
115 sleep_multiplier=0,
Fang Dengf24be082018-02-10 10:09:55 -0800116 retry_backoff_factor=1):
cylan0d77ae12018-05-18 08:36:48 +0000117 """Decorater which retries the function call if |retry_checker| returns true.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700118
cylan0d77ae12018-05-18 08:36:48 +0000119 Args:
120 retry_checker: A callback function which should take an exception instance
121 and return True if functor(*args, **kwargs) should be retried
122 when such exception is raised, and return False if it should
123 not be retried.
124 max_retries: Maximum number of retries allowed.
125 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
126 retry_backoff_factor is 1. Will sleep
127 sleep_multiplier * (
128 retry_backoff_factor ** (attempt_count - 1))
129 if retry_backoff_factor != 1.
130 retry_backoff_factor: See explanation of sleep_multiplier.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700131
cylan0d77ae12018-05-18 08:36:48 +0000132 Returns:
133 The function wrapper.
134 """
135
136 def _Wrapper(func):
137 def _FunctionWrapper(*args, **kwargs):
138 return Retry(retry_checker, max_retries, func, sleep_multiplier,
139 retry_backoff_factor, *args, **kwargs)
140
141 return _FunctionWrapper
142
143 return _Wrapper
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700144
145
cylan4f73c1f2018-07-19 16:40:31 +0800146def Retry(retry_checker, max_retries, functor, sleep_multiplier,
147 retry_backoff_factor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000148 """Conditionally retry a function.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700149
cylan0d77ae12018-05-18 08:36:48 +0000150 Args:
151 retry_checker: A callback function which should take an exception instance
152 and return True if functor(*args, **kwargs) should be retried
153 when such exception is raised, and return False if it should
154 not be retried.
155 max_retries: Maximum number of retries allowed.
156 functor: The function to call, will call functor(*args, **kwargs).
157 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
158 retry_backoff_factor is 1. Will sleep
159 sleep_multiplier * (
160 retry_backoff_factor ** (attempt_count - 1))
161 if retry_backoff_factor != 1.
162 retry_backoff_factor: See explanation of sleep_multiplier.
163 *args: Arguments to pass to the functor.
164 **kwargs: Key-val based arguments to pass to the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700165
cylan0d77ae12018-05-18 08:36:48 +0000166 Returns:
167 The return value of the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700168
cylan0d77ae12018-05-18 08:36:48 +0000169 Raises:
170 Exception: The exception that functor(*args, **kwargs) throws.
171 """
172 attempt_count = 0
173 while attempt_count <= max_retries:
174 try:
175 attempt_count += 1
176 return_value = functor(*args, **kwargs)
177 return return_value
178 except Exception as e: # pylint: disable=W0703
179 if retry_checker(e) and attempt_count <= max_retries:
180 if retry_backoff_factor != 1:
181 sleep = sleep_multiplier * (retry_backoff_factor**
182 (attempt_count - 1))
183 else:
184 sleep = sleep_multiplier * attempt_count
Kevin Chengd25feee2018-05-24 10:15:20 -0700185 time.sleep(sleep)
cylan0d77ae12018-05-18 08:36:48 +0000186 else:
187 raise
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700188
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700189
Fang Dengf24be082018-02-10 10:09:55 -0800190def RetryExceptionType(exception_types, max_retries, functor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000191 """Retry exception if it is one of the given types.
Fang Dengf24be082018-02-10 10:09:55 -0800192
cylan0d77ae12018-05-18 08:36:48 +0000193 Args:
194 exception_types: A tuple of exception types, e.g. (ValueError, KeyError)
195 max_retries: Max number of retries allowed.
196 functor: The function to call. Will be retried if exception is raised and
197 the exception is one of the exception_types.
198 *args: Arguments to pass to Retry function.
199 **kwargs: Key-val based arguments to pass to Retry functions.
Fang Dengf24be082018-02-10 10:09:55 -0800200
cylan0d77ae12018-05-18 08:36:48 +0000201 Returns:
202 The value returned by calling functor.
203 """
204 return Retry(lambda e: isinstance(e, exception_types), max_retries,
205 functor, *args, **kwargs)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700206
207
208def PollAndWait(func, expected_return, timeout_exception, timeout_secs,
209 sleep_interval_secs, *args, **kwargs):
210 """Call a function until the function returns expected value or times out.
211
212 Args:
213 func: Function to call.
214 expected_return: The expected return value.
215 timeout_exception: Exception to raise when it hits timeout.
216 timeout_secs: Timeout seconds.
217 If 0 or less than zero, the function will run once and
218 we will not wait on it.
219 sleep_interval_secs: Time to sleep between two attemps.
220 *args: list of args to pass to func.
221 **kwargs: dictionary of keyword based args to pass to func.
222
223 Raises:
224 timeout_exception: if the run of function times out.
225 """
226 # TODO(fdeng): Currently this method does not kill
227 # |func|, if |func| takes longer than |timeout_secs|.
228 # We can use a more robust version from chromite.
229 start = time.time()
230 while True:
231 return_value = func(*args, **kwargs)
232 if return_value == expected_return:
233 return
234 elif time.time() - start > timeout_secs:
235 raise timeout_exception
236 else:
237 if sleep_interval_secs > 0:
238 time.sleep(sleep_interval_secs)
239
240
241def GenerateUniqueName(prefix=None, suffix=None):
Sam Chiu81bdc652018-06-29 18:45:08 +0800242 """Generate a random unique name using uuid4.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700243
244 Args:
245 prefix: String, desired prefix to prepend to the generated name.
246 suffix: String, desired suffix to append to the generated name.
247
248 Returns:
249 String, a random name.
250 """
251 name = uuid.uuid4().hex
252 if prefix:
253 name = "-".join([prefix, name])
254 if suffix:
255 name = "-".join([name, suffix])
256 return name
257
258
259def MakeTarFile(src_dict, dest):
260 """Archive files in tar.gz format to a file named as |dest|.
261
262 Args:
263 src_dict: A dictionary that maps a path to be archived
264 to the corresponding name that appears in the archive.
265 dest: String, path to output file, e.g. /tmp/myfile.tar.gz
266 """
267 logger.info("Compressing %s into %s.", src_dict.keys(), dest)
268 with tarfile.open(dest, "w:gz") as tar:
269 for src, arcname in src_dict.iteritems():
270 tar.add(src, arcname=arcname)
271
272
Fang Deng69498c32017-03-02 14:29:30 -0800273def CreateSshKeyPairIfNotExist(private_key_path, public_key_path):
274 """Create the ssh key pair if they don't exist.
275
cylan4f73c1f2018-07-19 16:40:31 +0800276 Case1. If the private key doesn't exist, we will create both the public key
277 and the private key.
278 Case2. If the private key exists but public key doesn't, we will create the
279 public key by using the private key.
280 Case3. If the public key exists but the private key doesn't, we will create
281 a new private key and overwrite the public key.
Fang Deng69498c32017-03-02 14:29:30 -0800282
283 Args:
284 private_key_path: Path to the private key file.
285 e.g. ~/.ssh/acloud_rsa
286 public_key_path: Path to the public key file.
287 e.g. ~/.ssh/acloud_rsa.pub
cylan4f73c1f2018-07-19 16:40:31 +0800288
Fang Deng69498c32017-03-02 14:29:30 -0800289 Raises:
290 error.DriverError: If failed to create the key pair.
291 """
292 public_key_path = os.path.expanduser(public_key_path)
293 private_key_path = os.path.expanduser(private_key_path)
cylan4f73c1f2018-07-19 16:40:31 +0800294 public_key_exist = os.path.exists(public_key_path)
295 private_key_exist = os.path.exists(private_key_path)
296 if public_key_exist and private_key_exist:
cylan0d77ae12018-05-18 08:36:48 +0000297 logger.debug(
cylan4f73c1f2018-07-19 16:40:31 +0800298 "The ssh private key (%s) and public key (%s) already exist,"
cylan0d77ae12018-05-18 08:36:48 +0000299 "will not automatically create the key pairs.", private_key_path,
300 public_key_path)
Fang Deng69498c32017-03-02 14:29:30 -0800301 return
cylan4f73c1f2018-07-19 16:40:31 +0800302 key_folder = os.path.dirname(private_key_path)
303 if not os.path.exists(key_folder):
304 os.makedirs(key_folder)
Fang Deng69498c32017-03-02 14:29:30 -0800305 try:
cylan4f73c1f2018-07-19 16:40:31 +0800306 if private_key_exist:
307 cmd = SSH_KEYGEN_PUB_CMD + ["-f", private_key_path]
308 with open(public_key_path, 'w') as outfile:
309 stream_content = subprocess.check_output(cmd)
310 outfile.write(
311 stream_content.rstrip('\n') + " " + getpass.getuser())
312 logger.info(
313 "The ssh public key (%s) do not exist, "
314 "automatically creating public key, calling: %s",
315 public_key_path, " ".join(cmd))
316 else:
317 cmd = SSH_KEYGEN_CMD + [
318 "-C", getpass.getuser(), "-f", private_key_path
319 ]
320 logger.info(
321 "Creating public key from private key (%s) via cmd: %s",
322 private_key_path, " ".join(cmd))
323 subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout)
Fang Deng69498c32017-03-02 14:29:30 -0800324 except subprocess.CalledProcessError as e:
cylan0d77ae12018-05-18 08:36:48 +0000325 raise errors.DriverError("Failed to create ssh key pair: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800326 except OSError as e:
327 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000328 "Failed to create ssh key pair, please make sure "
329 "'ssh-keygen' is installed: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800330
331 # By default ssh-keygen will create a public key file
332 # by append .pub to the private key file name. Rename it
333 # to what's requested by public_key_path.
334 default_pub_key_path = "%s.pub" % private_key_path
335 try:
336 if default_pub_key_path != public_key_path:
337 os.rename(default_pub_key_path, public_key_path)
338 except OSError as e:
339 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000340 "Failed to rename %s to %s: %s" % (default_pub_key_path,
341 public_key_path, str(e)))
Fang Deng69498c32017-03-02 14:29:30 -0800342
343 logger.info("Created ssh private key (%s) and public key (%s)",
344 private_key_path, public_key_path)
345
346
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700347def VerifyRsaPubKey(rsa):
348 """Verify the format of rsa public key.
349
350 Args:
351 rsa: content of rsa public key. It should follow the format of
352 ssh-rsa AAAAB3NzaC1yc2EA.... test@test.com
353
354 Raises:
355 DriverError if the format is not correct.
356 """
357 if not rsa or not all(ord(c) < 128 for c in rsa):
358 raise errors.DriverError(
359 "rsa key is empty or contains non-ascii character: %s" % rsa)
360
361 elements = rsa.split()
362 if len(elements) != 3:
363 raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa)
364
365 key_type, data, _ = elements
366 try:
367 binary_data = base64.decodestring(data)
368 # number of bytes of int type
369 int_length = 4
370 # binary_data is like "7ssh-key..." in a binary format.
371 # The first 4 bytes should represent 7, which should be
372 # the length of the following string "ssh-key".
373 # And the next 7 bytes should be string "ssh-key".
374 # We will verify that the rsa conforms to this format.
375 # ">I" in the following line means "big-endian unsigned integer".
376 type_length = struct.unpack(">I", binary_data[:int_length])[0]
377 if binary_data[int_length:int_length + type_length] != key_type:
378 raise errors.DriverError("rsa key is invalid: %s" % rsa)
379 except (struct.error, binascii.Error) as e:
cylan0d77ae12018-05-18 08:36:48 +0000380 raise errors.DriverError(
381 "rsa key is invalid: %s, error: %s" % (rsa, str(e)))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700382
chojoycecd004bc2018-09-13 10:39:00 +0800383def Decompress(sourcefile, dest=None):
384 """Decompress .zip or .tar.gz.
385
386 Args:
387 sourcefile: A string, a source file path to decompress.
388 dest: A string, a folder path as decompress destination.
389
390 Raises:
391 errors.UnsupportedCompressionFileType: Not supported extension.
392 """
393 logger.info("Start to decompress %s!", sourcefile)
394 dest_path = dest if dest else "."
395 if sourcefile.endswith(".tar.gz"):
396 with tarfile.open(sourcefile, "r:gz") as compressor:
397 compressor.extractall(dest_path)
398 elif sourcefile.endswith(".zip"):
399 with zipfile.ZipFile(sourcefile, 'r') as compressor:
400 compressor.extractall(dest_path)
401 else:
402 raise root_errors.UnsupportedCompressionFileType(
403 "Sorry, we could only support compression file type "
404 "for zip or tar.gz.")
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700405
Sam Chiu81bdc652018-06-29 18:45:08 +0800406# pylint: disable=old-style-class,no-init
407class TextColors:
408 """A class that defines common color ANSI code."""
409
410 HEADER = "\033[95m"
411 OKBLUE = "\033[94m"
412 OKGREEN = "\033[92m"
413 WARNING = "\033[93m"
414 FAIL = "\033[91m"
415 ENDC = "\033[0m"
416 BOLD = "\033[1m"
417 UNDERLINE = "\033[4m"
418
419
herbertxuedf01c422018-09-06 19:52:52 +0800420def PrintColorString(message, colors=TextColors.OKBLUE, **kwargs):
Sam Chiu81bdc652018-06-29 18:45:08 +0800421 """A helper function to print out colored text.
422
herbertxuedf01c422018-09-06 19:52:52 +0800423 Use print function "print(message, end="")" to show message in one line.
424 Example code:
425 DisplayMessages("Creating GCE instance...", end="")
426 # Job execute 20s
427 DisplayMessages("Done! (20s)")
428 Display:
429 Creating GCE instance...
430 # After job finished, messages update as following:
431 Creating GCE instance...Done! (20s)
432
Sam Chiu81bdc652018-06-29 18:45:08 +0800433 Args:
434 message: String, the message text.
435 colors: String, color code.
herbertxuedf01c422018-09-06 19:52:52 +0800436 **kwargs: dictionary of keyword based args to pass to func.
Sam Chiu81bdc652018-06-29 18:45:08 +0800437 """
herbertxuedf01c422018-09-06 19:52:52 +0800438 print(colors + message + TextColors.ENDC, **kwargs)
439 sys.stdout.flush()
Sam Chiu81bdc652018-06-29 18:45:08 +0800440
441
442def InteractWithQuestion(question, colors=TextColors.WARNING):
443 """A helper function to define the common way to run interactive cmd.
444
445 Args:
446 question: String, the question to ask user.
447 colors: String, color code.
448
449 Returns:
450 String, input from user.
451 """
452 return str(raw_input(colors + question + TextColors.ENDC).strip())
453
herbertxuedf01c422018-09-06 19:52:52 +0800454
herbertxue34776bb2018-07-03 21:57:48 +0800455def GetUserAnswerYes(question):
456 """Ask user about acloud setup question.
457
458 Args:
459 question: String, ask question for user.
460 Ex: "Are you sure to change bucket name:[y/n]"
461
462 Returns:
463 Boolean, True if answer is "Yes", False otherwise.
464 """
465 answer = InteractWithQuestion(question)
466 return answer.lower() in constants.USER_ANSWER_YES
467
Sam Chiu81bdc652018-06-29 18:45:08 +0800468
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700469class BatchHttpRequestExecutor(object):
470 """A helper class that executes requests in batch with retry.
471
472 This executor executes http requests in a batch and retry
473 those that have failed. It iteratively updates the dictionary
474 self._final_results with latest results, which can be retrieved
475 via GetResults.
476 """
477
478 def __init__(self,
479 execute_once_functor,
480 requests,
481 retry_http_codes=None,
482 max_retry=None,
483 sleep=None,
484 backoff_factor=None,
485 other_retriable_errors=None):
486 """Initializes the executor.
487
488 Args:
489 execute_once_functor: A function that execute requests in batch once.
490 It should return a dictionary like
491 {request_id: (response, exception)}
492 requests: A dictionary where key is request id picked by caller,
493 and value is a apiclient.http.HttpRequest.
494 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800495 max_retry: See utils.Retry.
496 sleep: See utils.Retry.
497 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700498 other_retriable_errors: A tuple of error types that should be retried
499 other than errors.HttpError.
500 """
501 self._execute_once_functor = execute_once_functor
502 self._requests = requests
503 # A dictionary that maps request id to pending request.
504 self._pending_requests = {}
505 # A dictionary that maps request id to a tuple (response, exception).
506 self._final_results = {}
507 self._retry_http_codes = retry_http_codes
508 self._max_retry = max_retry
509 self._sleep = sleep
510 self._backoff_factor = backoff_factor
511 self._other_retriable_errors = other_retriable_errors
512
513 def _ShoudRetry(self, exception):
Sam Chiu81bdc652018-06-29 18:45:08 +0800514 """Check if an exception is retriable.
515
516 Args:
517 exception: An exception instance.
518 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700519 if isinstance(exception, self._other_retriable_errors):
520 return True
521
cylan0d77ae12018-05-18 08:36:48 +0000522 if (isinstance(exception, errors.HttpError)
523 and exception.code in self._retry_http_codes):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700524 return True
525 return False
526
527 def _ExecuteOnce(self):
528 """Executes pending requests and update it with failed, retriable ones.
529
530 Raises:
531 HasRetriableRequestsError: if some requests fail and are retriable.
532 """
533 results = self._execute_once_functor(self._pending_requests)
534 # Update final_results with latest results.
535 self._final_results.update(results)
536 # Clear pending_requests
537 self._pending_requests.clear()
538 for request_id, result in results.iteritems():
539 exception = result[1]
540 if exception is not None and self._ShoudRetry(exception):
541 # If this is a retriable exception, put it in pending_requests
542 self._pending_requests[request_id] = self._requests[request_id]
543 if self._pending_requests:
544 # If there is still retriable requests pending, raise an error
Fang Dengf24be082018-02-10 10:09:55 -0800545 # so that Retry will retry this function with pending_requests.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700546 raise errors.HasRetriableRequestsError(
cylan0d77ae12018-05-18 08:36:48 +0000547 "Retriable errors: %s" %
548 [str(results[rid][1]) for rid in self._pending_requests])
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700549
550 def Execute(self):
551 """Executes the requests and retry if necessary.
552
553 Will populate self._final_results.
554 """
cylan0d77ae12018-05-18 08:36:48 +0000555
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700556 def _ShouldRetryHandler(exc):
557 """Check if |exc| is a retriable exception.
558
559 Args:
560 exc: An exception.
561
562 Returns:
563 True if exception is of type HasRetriableRequestsError; False otherwise.
564 """
565 should_retry = isinstance(exc, errors.HasRetriableRequestsError)
566 if should_retry:
567 logger.info("Will retry failed requests.", exc_info=True)
568 logger.info("%s", exc)
569 return should_retry
570
571 try:
572 self._pending_requests = self._requests.copy()
Fang Dengf24be082018-02-10 10:09:55 -0800573 Retry(
cylan0d77ae12018-05-18 08:36:48 +0000574 _ShouldRetryHandler,
575 max_retries=self._max_retry,
Fang Dengf24be082018-02-10 10:09:55 -0800576 functor=self._ExecuteOnce,
577 sleep_multiplier=self._sleep,
578 retry_backoff_factor=self._backoff_factor)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700579 except errors.HasRetriableRequestsError:
580 logger.debug("Some requests did not succeed after retry.")
581
582 def GetResults(self):
583 """Returns final results.
584
585 Returns:
586 results, a dictionary in the following format
587 {request_id: (response, exception)}
588 request_ids are those from requests; response
589 is the http response for the request or None on error;
590 exception is an instance of DriverError or None if no error.
591 """
592 return self._final_results
cylan31fc5332018-09-17 22:12:08 +0800593
594
595class TimeExecute(object):
596 """Count the function execute time."""
597
598 def __init__(self, function_description=None, print_before_call=True, print_status=True):
599 """Initializes the class.
600
601 Args:
602 function_description: String that describes function (e.g."Creating
603 Instance...")
604 print_before_call: Boolean, print the function description before
605 calling the function, default True.
606 print_status: Boolean, print the status of the function after the
607 function has completed, default True ("OK" or "Fail").
608 """
609 self._function_description = function_description
610 self._print_before_call = print_before_call
611 self._print_status = print_status
612
613 def __call__(self, func):
614 def DecoratorFunction(*args, **kargs):
615 """Decorator function.
616
617 Args:
618 *args: Arguments to pass to the functor.
619 **kwargs: Key-val based arguments to pass to the functor.
620
621 Raises:
622 Exception: The exception that functor(*args, **kwargs) throws.
623 """
624 timestart = time.time()
625 if self._print_before_call:
626 PrintColorString("%s ..."% self._function_description, end="")
627 try:
628 result = func(*args, **kargs)
629 if not self._print_before_call:
630 PrintColorString("%s (%ds)" % (self._function_description,
631 time.time()-timestart),
632 TextColors.OKGREEN)
633 if self._print_status:
634 PrintColorString("OK! (%ds)" % (time.time()-timestart),
635 TextColors.OKGREEN)
636 return result
637 except:
638 if self._print_status:
639 PrintColorString("Fail! (%ds)" % (time.time()-timestart),
640 TextColors.FAIL)
641 raise
642 return DecoratorFunction
cylan66713722018-10-06 01:38:26 +0800643
644
645def PickFreePort():
646 """Helper to pick a free port.
647
648 Returns:
649 Integer, a free port number.
650 """
651 tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
652 tcp_socket.bind(("", 0))
653 port = tcp_socket.getsockname()[1]
654 tcp_socket.close()
655 return port
656
657
658def _ExecuteCommand(cmd, args):
659 """Execute command.
660
661 Args:
662 cmd: Strings of execute binary name.
663 args: List of args to pass in with cmd.
664
665 Raises:
666 errors.NoExecuteBin: Can't find the execute bin file.
667 """
668 bin_path = find_executable(cmd)
669 if not bin_path:
670 raise root_errors.NoExecuteCmd("unable to locate %s" % cmd)
671 command = [bin_path] + args
672 logger.debug("Running '%s'", ' '.join(command))
673 with open(os.devnull, "w") as dev_null:
674 subprocess.check_call(command, stderr=dev_null, stdout=dev_null)
675
676
677# pylint: disable=too-many-locals
678def AutoConnect(ip_addr, rsa_key_file, target_vnc_port, target_adb_port, ssh_user):
679 """Autoconnect to an AVD instance.
680
681 Args:
682 ip_addr: String, use to build the adb & vnc tunnel between local
683 and remote instance.
684 rsa_key_file: String, Private key file path to use when creating
685 the ssh tunnels.
686 target_vnc_port: Integer of target vnc port number.
687 target_adb_port: Integer of target adb port number.
688 ssh_user: String of user login into the instance.
689
690 Returns:
691 NamedTuple of (vnc_port, adb_port) SSHTUNNEL of the connect, both are
692 integers.
693 """
694 local_free_vnc_port = PickFreePort()
695 local_free_adb_port = PickFreePort()
696 try:
697 ssh_tunnel_args = _SSH_TUNNEL_ARGS % {
698 "rsa_key_file": rsa_key_file,
699 "vnc_port": local_free_vnc_port,
700 "adb_port": local_free_adb_port,
701 "target_vnc_port": target_vnc_port,
702 "target_adb_port": target_adb_port,
703 "ssh_user": ssh_user,
704 "ip_addr": ip_addr}
705 _ExecuteCommand(SSH_BIN, ssh_tunnel_args.split())
706 except subprocess.CalledProcessError:
707 PrintColorString("Failed to create ssh tunnels, retry with '#acloud "
708 "reconnect'.", TextColors.FAIL)
709 try:
710 adb_connect_args = _ADB_CONNECT_ARGS % {"adb_port": local_free_adb_port}
711 _ExecuteCommand(ADB_BIN, adb_connect_args.split())
712 except subprocess.CalledProcessError:
713 PrintColorString("Failed to adb connect, retry with "
714 "'#acloud reconnect'", TextColors.FAIL)
715
716 return ForwardedPorts(vnc_port=local_free_vnc_port,
717 adb_port=local_free_adb_port)
Kevin Chengeb85e862018-10-09 15:35:13 -0700718
719
720def GetAnswerFromList(answer_list, enable_choose_all=False):
721 """Get answer from a list.
722
723 Args:
724 answer_list: list of the answers to choose from.
725
726 Return:
727 List holding the answer(s).
728 """
729 print("[0] to exit.")
730 start_index = 1
731 if enable_choose_all:
732 start_index = 2
733 print("[1] for all.")
734 for num, item in enumerate(answer_list, start_index):
735 print("[%d] %s" % (num, item))
736
737 choice = -1
738 max_choice = len(answer_list) + 1
739 while True:
740 try:
741 choice = raw_input("Enter your choice[0-%d]: " % max_choice)
742 choice = int(choice)
743 except ValueError:
744 print("'%s' is not a valid integer.", choice)
745 continue
746 # Filter out choices
747 if choice == 0:
748 print("Exiting acloud.")
749 sys.exit()
750 if enable_choose_all and choice == 1:
751 return answer_list
752 if choice < 0 or choice > max_choice:
753 print("please choose between 0 and %d" % max_choice)
754 else:
755 return [answer_list[choice-start_index]]