blob: fd12c31a6e8502c47937ed1a7d849c321f5ebcfc [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 +080050_SSH_TUNNEL_ARGS = ("-i %(rsa_key_file)s -o UserKnownHostsFile=/dev/null "
51 "-o StrictHostKeyChecking=no "
52 "-L %(vnc_port)d:127.0.0.1:%(target_vnc_port)d "
53 "-L %(adb_port)d:127.0.0.1:%(target_adb_port)d "
54 "-N -f -l %(ssh_user)s %(ip_addr)s")
cylan66713722018-10-06 01:38:26 +080055_ADB_CONNECT_ARGS = "connect 127.0.0.1:%(adb_port)d"
56# Store the ports that vnc/adb are forwarded to, both are integers.
57ForwardedPorts = collections.namedtuple("ForwardedPorts", [constants.VNC_PORT,
58 constants.ADB_PORT])
Fang Deng69498c32017-03-02 14:29:30 -080059
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070060class TempDir(object):
Fang Deng26e4dc12018-03-04 19:01:59 -080061 """A context manager that ceates a temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070062
Fang Deng26e4dc12018-03-04 19:01:59 -080063 Attributes:
Sam Chiu81bdc652018-06-29 18:45:08 +080064 path: The path of the temporary directory.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070065 """
66
Fang Deng26e4dc12018-03-04 19:01:59 -080067 def __init__(self):
68 self.path = tempfile.mkdtemp()
69 os.chmod(self.path, 0o700)
70 logger.debug("Created temporary dir %s", self.path)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070071
72 def __enter__(self):
Fang Deng26e4dc12018-03-04 19:01:59 -080073 """Enter."""
74 return self.path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070075
Fang Deng26e4dc12018-03-04 19:01:59 -080076 def __exit__(self, exc_type, exc_value, traceback):
77 """Exit.
78
79 Args:
80 exc_type: Exception type raised within the context manager.
81 None if no execption is raised.
82 exc_value: Exception instance raised within the context manager.
83 None if no execption is raised.
84 traceback: Traceback for exeception that is raised within
85 the context manager.
86 None if no execption is raised.
87 Raises:
88 EnvironmentError or OSError when failed to delete temp directory.
89 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070090 try:
Fang Deng26e4dc12018-03-04 19:01:59 -080091 if self.path:
92 shutil.rmtree(self.path)
93 logger.debug("Deleted temporary dir %s", self.path)
94 except EnvironmentError as e:
95 # Ignore error if there is no exception raised
96 # within the with-clause and the EnvironementError is
97 # about problem that directory or file does not exist.
98 if not exc_type and e.errno != errno.ENOENT:
99 raise
100 except Exception as e: # pylint: disable=W0703
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700101 if exc_type:
Fang Deng26e4dc12018-03-04 19:01:59 -0800102 logger.error(
cylan0d77ae12018-05-18 08:36:48 +0000103 "Encountered error while deleting %s: %s",
104 self.path,
105 str(e),
106 exc_info=True)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700107 else:
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700108 raise
109
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700110
cylan0d77ae12018-05-18 08:36:48 +0000111def RetryOnException(retry_checker,
112 max_retries,
113 sleep_multiplier=0,
Fang Dengf24be082018-02-10 10:09:55 -0800114 retry_backoff_factor=1):
cylan0d77ae12018-05-18 08:36:48 +0000115 """Decorater which retries the function call if |retry_checker| returns true.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700116
cylan0d77ae12018-05-18 08:36:48 +0000117 Args:
118 retry_checker: A callback function which should take an exception instance
119 and return True if functor(*args, **kwargs) should be retried
120 when such exception is raised, and return False if it should
121 not be retried.
122 max_retries: Maximum number of retries allowed.
123 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
124 retry_backoff_factor is 1. Will sleep
125 sleep_multiplier * (
126 retry_backoff_factor ** (attempt_count - 1))
127 if retry_backoff_factor != 1.
128 retry_backoff_factor: See explanation of sleep_multiplier.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700129
cylan0d77ae12018-05-18 08:36:48 +0000130 Returns:
131 The function wrapper.
132 """
133
134 def _Wrapper(func):
135 def _FunctionWrapper(*args, **kwargs):
136 return Retry(retry_checker, max_retries, func, sleep_multiplier,
137 retry_backoff_factor, *args, **kwargs)
138
139 return _FunctionWrapper
140
141 return _Wrapper
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700142
143
cylan4f73c1f2018-07-19 16:40:31 +0800144def Retry(retry_checker, max_retries, functor, sleep_multiplier,
145 retry_backoff_factor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000146 """Conditionally retry a function.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700147
cylan0d77ae12018-05-18 08:36:48 +0000148 Args:
149 retry_checker: A callback function which should take an exception instance
150 and return True if functor(*args, **kwargs) should be retried
151 when such exception is raised, and return False if it should
152 not be retried.
153 max_retries: Maximum number of retries allowed.
154 functor: The function to call, will call functor(*args, **kwargs).
155 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
156 retry_backoff_factor is 1. Will sleep
157 sleep_multiplier * (
158 retry_backoff_factor ** (attempt_count - 1))
159 if retry_backoff_factor != 1.
160 retry_backoff_factor: See explanation of sleep_multiplier.
161 *args: Arguments to pass to the functor.
162 **kwargs: Key-val based arguments to pass to the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700163
cylan0d77ae12018-05-18 08:36:48 +0000164 Returns:
165 The return value of the functor.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700166
cylan0d77ae12018-05-18 08:36:48 +0000167 Raises:
168 Exception: The exception that functor(*args, **kwargs) throws.
169 """
170 attempt_count = 0
171 while attempt_count <= max_retries:
172 try:
173 attempt_count += 1
174 return_value = functor(*args, **kwargs)
175 return return_value
176 except Exception as e: # pylint: disable=W0703
177 if retry_checker(e) and attempt_count <= max_retries:
178 if retry_backoff_factor != 1:
179 sleep = sleep_multiplier * (retry_backoff_factor**
180 (attempt_count - 1))
181 else:
182 sleep = sleep_multiplier * attempt_count
Kevin Chengd25feee2018-05-24 10:15:20 -0700183 time.sleep(sleep)
cylan0d77ae12018-05-18 08:36:48 +0000184 else:
185 raise
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700186
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700187
Fang Dengf24be082018-02-10 10:09:55 -0800188def RetryExceptionType(exception_types, max_retries, functor, *args, **kwargs):
cylan0d77ae12018-05-18 08:36:48 +0000189 """Retry exception if it is one of the given types.
Fang Dengf24be082018-02-10 10:09:55 -0800190
cylan0d77ae12018-05-18 08:36:48 +0000191 Args:
192 exception_types: A tuple of exception types, e.g. (ValueError, KeyError)
193 max_retries: Max number of retries allowed.
194 functor: The function to call. Will be retried if exception is raised and
195 the exception is one of the exception_types.
196 *args: Arguments to pass to Retry function.
197 **kwargs: Key-val based arguments to pass to Retry functions.
Fang Dengf24be082018-02-10 10:09:55 -0800198
cylan0d77ae12018-05-18 08:36:48 +0000199 Returns:
200 The value returned by calling functor.
201 """
202 return Retry(lambda e: isinstance(e, exception_types), max_retries,
203 functor, *args, **kwargs)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700204
205
206def PollAndWait(func, expected_return, timeout_exception, timeout_secs,
207 sleep_interval_secs, *args, **kwargs):
208 """Call a function until the function returns expected value or times out.
209
210 Args:
211 func: Function to call.
212 expected_return: The expected return value.
213 timeout_exception: Exception to raise when it hits timeout.
214 timeout_secs: Timeout seconds.
215 If 0 or less than zero, the function will run once and
216 we will not wait on it.
217 sleep_interval_secs: Time to sleep between two attemps.
218 *args: list of args to pass to func.
219 **kwargs: dictionary of keyword based args to pass to func.
220
221 Raises:
222 timeout_exception: if the run of function times out.
223 """
224 # TODO(fdeng): Currently this method does not kill
225 # |func|, if |func| takes longer than |timeout_secs|.
226 # We can use a more robust version from chromite.
227 start = time.time()
228 while True:
229 return_value = func(*args, **kwargs)
230 if return_value == expected_return:
231 return
232 elif time.time() - start > timeout_secs:
233 raise timeout_exception
234 else:
235 if sleep_interval_secs > 0:
236 time.sleep(sleep_interval_secs)
237
238
239def GenerateUniqueName(prefix=None, suffix=None):
Sam Chiu81bdc652018-06-29 18:45:08 +0800240 """Generate a random unique name using uuid4.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700241
242 Args:
243 prefix: String, desired prefix to prepend to the generated name.
244 suffix: String, desired suffix to append to the generated name.
245
246 Returns:
247 String, a random name.
248 """
249 name = uuid.uuid4().hex
250 if prefix:
251 name = "-".join([prefix, name])
252 if suffix:
253 name = "-".join([name, suffix])
254 return name
255
256
257def MakeTarFile(src_dict, dest):
258 """Archive files in tar.gz format to a file named as |dest|.
259
260 Args:
261 src_dict: A dictionary that maps a path to be archived
262 to the corresponding name that appears in the archive.
263 dest: String, path to output file, e.g. /tmp/myfile.tar.gz
264 """
265 logger.info("Compressing %s into %s.", src_dict.keys(), dest)
266 with tarfile.open(dest, "w:gz") as tar:
267 for src, arcname in src_dict.iteritems():
268 tar.add(src, arcname=arcname)
269
270
Fang Deng69498c32017-03-02 14:29:30 -0800271def CreateSshKeyPairIfNotExist(private_key_path, public_key_path):
272 """Create the ssh key pair if they don't exist.
273
cylan4f73c1f2018-07-19 16:40:31 +0800274 Case1. If the private key doesn't exist, we will create both the public key
275 and the private key.
276 Case2. If the private key exists but public key doesn't, we will create the
277 public key by using the private key.
278 Case3. If the public key exists but the private key doesn't, we will create
279 a new private key and overwrite the public key.
Fang Deng69498c32017-03-02 14:29:30 -0800280
281 Args:
282 private_key_path: Path to the private key file.
283 e.g. ~/.ssh/acloud_rsa
284 public_key_path: Path to the public key file.
285 e.g. ~/.ssh/acloud_rsa.pub
cylan4f73c1f2018-07-19 16:40:31 +0800286
Fang Deng69498c32017-03-02 14:29:30 -0800287 Raises:
288 error.DriverError: If failed to create the key pair.
289 """
290 public_key_path = os.path.expanduser(public_key_path)
291 private_key_path = os.path.expanduser(private_key_path)
cylan4f73c1f2018-07-19 16:40:31 +0800292 public_key_exist = os.path.exists(public_key_path)
293 private_key_exist = os.path.exists(private_key_path)
294 if public_key_exist and private_key_exist:
cylan0d77ae12018-05-18 08:36:48 +0000295 logger.debug(
cylan4f73c1f2018-07-19 16:40:31 +0800296 "The ssh private key (%s) and public key (%s) already exist,"
cylan0d77ae12018-05-18 08:36:48 +0000297 "will not automatically create the key pairs.", private_key_path,
298 public_key_path)
Fang Deng69498c32017-03-02 14:29:30 -0800299 return
cylan4f73c1f2018-07-19 16:40:31 +0800300 key_folder = os.path.dirname(private_key_path)
301 if not os.path.exists(key_folder):
302 os.makedirs(key_folder)
Fang Deng69498c32017-03-02 14:29:30 -0800303 try:
cylan4f73c1f2018-07-19 16:40:31 +0800304 if private_key_exist:
305 cmd = SSH_KEYGEN_PUB_CMD + ["-f", private_key_path]
306 with open(public_key_path, 'w') as outfile:
307 stream_content = subprocess.check_output(cmd)
308 outfile.write(
309 stream_content.rstrip('\n') + " " + getpass.getuser())
310 logger.info(
311 "The ssh public key (%s) do not exist, "
312 "automatically creating public key, calling: %s",
313 public_key_path, " ".join(cmd))
314 else:
315 cmd = SSH_KEYGEN_CMD + [
316 "-C", getpass.getuser(), "-f", private_key_path
317 ]
318 logger.info(
319 "Creating public key from private key (%s) via cmd: %s",
320 private_key_path, " ".join(cmd))
321 subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout)
Fang Deng69498c32017-03-02 14:29:30 -0800322 except subprocess.CalledProcessError as e:
cylan0d77ae12018-05-18 08:36:48 +0000323 raise errors.DriverError("Failed to create ssh key pair: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800324 except OSError as e:
325 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000326 "Failed to create ssh key pair, please make sure "
327 "'ssh-keygen' is installed: %s" % str(e))
Fang Deng69498c32017-03-02 14:29:30 -0800328
329 # By default ssh-keygen will create a public key file
330 # by append .pub to the private key file name. Rename it
331 # to what's requested by public_key_path.
332 default_pub_key_path = "%s.pub" % private_key_path
333 try:
334 if default_pub_key_path != public_key_path:
335 os.rename(default_pub_key_path, public_key_path)
336 except OSError as e:
337 raise errors.DriverError(
cylan0d77ae12018-05-18 08:36:48 +0000338 "Failed to rename %s to %s: %s" % (default_pub_key_path,
339 public_key_path, str(e)))
Fang Deng69498c32017-03-02 14:29:30 -0800340
341 logger.info("Created ssh private key (%s) and public key (%s)",
342 private_key_path, public_key_path)
343
344
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700345def VerifyRsaPubKey(rsa):
346 """Verify the format of rsa public key.
347
348 Args:
349 rsa: content of rsa public key. It should follow the format of
350 ssh-rsa AAAAB3NzaC1yc2EA.... test@test.com
351
352 Raises:
353 DriverError if the format is not correct.
354 """
355 if not rsa or not all(ord(c) < 128 for c in rsa):
356 raise errors.DriverError(
357 "rsa key is empty or contains non-ascii character: %s" % rsa)
358
359 elements = rsa.split()
360 if len(elements) != 3:
361 raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa)
362
363 key_type, data, _ = elements
364 try:
365 binary_data = base64.decodestring(data)
366 # number of bytes of int type
367 int_length = 4
368 # binary_data is like "7ssh-key..." in a binary format.
369 # The first 4 bytes should represent 7, which should be
370 # the length of the following string "ssh-key".
371 # And the next 7 bytes should be string "ssh-key".
372 # We will verify that the rsa conforms to this format.
373 # ">I" in the following line means "big-endian unsigned integer".
374 type_length = struct.unpack(">I", binary_data[:int_length])[0]
375 if binary_data[int_length:int_length + type_length] != key_type:
376 raise errors.DriverError("rsa key is invalid: %s" % rsa)
377 except (struct.error, binascii.Error) as e:
cylan0d77ae12018-05-18 08:36:48 +0000378 raise errors.DriverError(
379 "rsa key is invalid: %s, error: %s" % (rsa, str(e)))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700380
chojoycecd004bc2018-09-13 10:39:00 +0800381def Decompress(sourcefile, dest=None):
382 """Decompress .zip or .tar.gz.
383
384 Args:
385 sourcefile: A string, a source file path to decompress.
386 dest: A string, a folder path as decompress destination.
387
388 Raises:
389 errors.UnsupportedCompressionFileType: Not supported extension.
390 """
391 logger.info("Start to decompress %s!", sourcefile)
392 dest_path = dest if dest else "."
393 if sourcefile.endswith(".tar.gz"):
394 with tarfile.open(sourcefile, "r:gz") as compressor:
395 compressor.extractall(dest_path)
396 elif sourcefile.endswith(".zip"):
397 with zipfile.ZipFile(sourcefile, 'r') as compressor:
398 compressor.extractall(dest_path)
399 else:
400 raise root_errors.UnsupportedCompressionFileType(
401 "Sorry, we could only support compression file type "
402 "for zip or tar.gz.")
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700403
Sam Chiu81bdc652018-06-29 18:45:08 +0800404# pylint: disable=old-style-class,no-init
405class TextColors:
406 """A class that defines common color ANSI code."""
407
408 HEADER = "\033[95m"
409 OKBLUE = "\033[94m"
410 OKGREEN = "\033[92m"
411 WARNING = "\033[93m"
412 FAIL = "\033[91m"
413 ENDC = "\033[0m"
414 BOLD = "\033[1m"
415 UNDERLINE = "\033[4m"
416
417
herbertxuedf01c422018-09-06 19:52:52 +0800418def PrintColorString(message, colors=TextColors.OKBLUE, **kwargs):
Sam Chiu81bdc652018-06-29 18:45:08 +0800419 """A helper function to print out colored text.
420
herbertxuedf01c422018-09-06 19:52:52 +0800421 Use print function "print(message, end="")" to show message in one line.
422 Example code:
423 DisplayMessages("Creating GCE instance...", end="")
424 # Job execute 20s
425 DisplayMessages("Done! (20s)")
426 Display:
427 Creating GCE instance...
428 # After job finished, messages update as following:
429 Creating GCE instance...Done! (20s)
430
Sam Chiu81bdc652018-06-29 18:45:08 +0800431 Args:
432 message: String, the message text.
433 colors: String, color code.
herbertxuedf01c422018-09-06 19:52:52 +0800434 **kwargs: dictionary of keyword based args to pass to func.
Sam Chiu81bdc652018-06-29 18:45:08 +0800435 """
herbertxuedf01c422018-09-06 19:52:52 +0800436 print(colors + message + TextColors.ENDC, **kwargs)
437 sys.stdout.flush()
Sam Chiu81bdc652018-06-29 18:45:08 +0800438
439
440def InteractWithQuestion(question, colors=TextColors.WARNING):
441 """A helper function to define the common way to run interactive cmd.
442
443 Args:
444 question: String, the question to ask user.
445 colors: String, color code.
446
447 Returns:
448 String, input from user.
449 """
450 return str(raw_input(colors + question + TextColors.ENDC).strip())
451
herbertxuedf01c422018-09-06 19:52:52 +0800452
herbertxue34776bb2018-07-03 21:57:48 +0800453def GetUserAnswerYes(question):
454 """Ask user about acloud setup question.
455
456 Args:
457 question: String, ask question for user.
458 Ex: "Are you sure to change bucket name:[y/n]"
459
460 Returns:
461 Boolean, True if answer is "Yes", False otherwise.
462 """
463 answer = InteractWithQuestion(question)
464 return answer.lower() in constants.USER_ANSWER_YES
465
Sam Chiu81bdc652018-06-29 18:45:08 +0800466
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700467class BatchHttpRequestExecutor(object):
468 """A helper class that executes requests in batch with retry.
469
470 This executor executes http requests in a batch and retry
471 those that have failed. It iteratively updates the dictionary
472 self._final_results with latest results, which can be retrieved
473 via GetResults.
474 """
475
476 def __init__(self,
477 execute_once_functor,
478 requests,
479 retry_http_codes=None,
480 max_retry=None,
481 sleep=None,
482 backoff_factor=None,
483 other_retriable_errors=None):
484 """Initializes the executor.
485
486 Args:
487 execute_once_functor: A function that execute requests in batch once.
488 It should return a dictionary like
489 {request_id: (response, exception)}
490 requests: A dictionary where key is request id picked by caller,
491 and value is a apiclient.http.HttpRequest.
492 retry_http_codes: A list of http codes to retry.
Fang Dengf24be082018-02-10 10:09:55 -0800493 max_retry: See utils.Retry.
494 sleep: See utils.Retry.
495 backoff_factor: See utils.Retry.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700496 other_retriable_errors: A tuple of error types that should be retried
497 other than errors.HttpError.
498 """
499 self._execute_once_functor = execute_once_functor
500 self._requests = requests
501 # A dictionary that maps request id to pending request.
502 self._pending_requests = {}
503 # A dictionary that maps request id to a tuple (response, exception).
504 self._final_results = {}
505 self._retry_http_codes = retry_http_codes
506 self._max_retry = max_retry
507 self._sleep = sleep
508 self._backoff_factor = backoff_factor
509 self._other_retriable_errors = other_retriable_errors
510
511 def _ShoudRetry(self, exception):
Sam Chiu81bdc652018-06-29 18:45:08 +0800512 """Check if an exception is retriable.
513
514 Args:
515 exception: An exception instance.
516 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700517 if isinstance(exception, self._other_retriable_errors):
518 return True
519
cylan0d77ae12018-05-18 08:36:48 +0000520 if (isinstance(exception, errors.HttpError)
521 and exception.code in self._retry_http_codes):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700522 return True
523 return False
524
525 def _ExecuteOnce(self):
526 """Executes pending requests and update it with failed, retriable ones.
527
528 Raises:
529 HasRetriableRequestsError: if some requests fail and are retriable.
530 """
531 results = self._execute_once_functor(self._pending_requests)
532 # Update final_results with latest results.
533 self._final_results.update(results)
534 # Clear pending_requests
535 self._pending_requests.clear()
536 for request_id, result in results.iteritems():
537 exception = result[1]
538 if exception is not None and self._ShoudRetry(exception):
539 # If this is a retriable exception, put it in pending_requests
540 self._pending_requests[request_id] = self._requests[request_id]
541 if self._pending_requests:
542 # If there is still retriable requests pending, raise an error
Fang Dengf24be082018-02-10 10:09:55 -0800543 # so that Retry will retry this function with pending_requests.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700544 raise errors.HasRetriableRequestsError(
cylan0d77ae12018-05-18 08:36:48 +0000545 "Retriable errors: %s" %
546 [str(results[rid][1]) for rid in self._pending_requests])
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700547
548 def Execute(self):
549 """Executes the requests and retry if necessary.
550
551 Will populate self._final_results.
552 """
cylan0d77ae12018-05-18 08:36:48 +0000553
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700554 def _ShouldRetryHandler(exc):
555 """Check if |exc| is a retriable exception.
556
557 Args:
558 exc: An exception.
559
560 Returns:
561 True if exception is of type HasRetriableRequestsError; False otherwise.
562 """
563 should_retry = isinstance(exc, errors.HasRetriableRequestsError)
564 if should_retry:
565 logger.info("Will retry failed requests.", exc_info=True)
566 logger.info("%s", exc)
567 return should_retry
568
569 try:
570 self._pending_requests = self._requests.copy()
Fang Dengf24be082018-02-10 10:09:55 -0800571 Retry(
cylan0d77ae12018-05-18 08:36:48 +0000572 _ShouldRetryHandler,
573 max_retries=self._max_retry,
Fang Dengf24be082018-02-10 10:09:55 -0800574 functor=self._ExecuteOnce,
575 sleep_multiplier=self._sleep,
576 retry_backoff_factor=self._backoff_factor)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700577 except errors.HasRetriableRequestsError:
578 logger.debug("Some requests did not succeed after retry.")
579
580 def GetResults(self):
581 """Returns final results.
582
583 Returns:
584 results, a dictionary in the following format
585 {request_id: (response, exception)}
586 request_ids are those from requests; response
587 is the http response for the request or None on error;
588 exception is an instance of DriverError or None if no error.
589 """
590 return self._final_results
cylan31fc5332018-09-17 22:12:08 +0800591
592
593class TimeExecute(object):
594 """Count the function execute time."""
595
596 def __init__(self, function_description=None, print_before_call=True, print_status=True):
597 """Initializes the class.
598
599 Args:
600 function_description: String that describes function (e.g."Creating
601 Instance...")
602 print_before_call: Boolean, print the function description before
603 calling the function, default True.
604 print_status: Boolean, print the status of the function after the
605 function has completed, default True ("OK" or "Fail").
606 """
607 self._function_description = function_description
608 self._print_before_call = print_before_call
609 self._print_status = print_status
610
611 def __call__(self, func):
612 def DecoratorFunction(*args, **kargs):
613 """Decorator function.
614
615 Args:
616 *args: Arguments to pass to the functor.
617 **kwargs: Key-val based arguments to pass to the functor.
618
619 Raises:
620 Exception: The exception that functor(*args, **kwargs) throws.
621 """
622 timestart = time.time()
623 if self._print_before_call:
624 PrintColorString("%s ..."% self._function_description, end="")
625 try:
626 result = func(*args, **kargs)
627 if not self._print_before_call:
628 PrintColorString("%s (%ds)" % (self._function_description,
629 time.time()-timestart),
630 TextColors.OKGREEN)
631 if self._print_status:
632 PrintColorString("OK! (%ds)" % (time.time()-timestart),
633 TextColors.OKGREEN)
634 return result
635 except:
636 if self._print_status:
637 PrintColorString("Fail! (%ds)" % (time.time()-timestart),
638 TextColors.FAIL)
639 raise
640 return DecoratorFunction
cylan66713722018-10-06 01:38:26 +0800641
642
643def PickFreePort():
644 """Helper to pick a free port.
645
646 Returns:
647 Integer, a free port number.
648 """
649 tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
650 tcp_socket.bind(("", 0))
651 port = tcp_socket.getsockname()[1]
652 tcp_socket.close()
653 return port
654
655
656def _ExecuteCommand(cmd, args):
657 """Execute command.
658
659 Args:
660 cmd: Strings of execute binary name.
661 args: List of args to pass in with cmd.
662
663 Raises:
664 errors.NoExecuteBin: Can't find the execute bin file.
665 """
666 bin_path = find_executable(cmd)
667 if not bin_path:
668 raise root_errors.NoExecuteCmd("unable to locate %s" % cmd)
669 command = [bin_path] + args
670 logger.debug("Running '%s'", ' '.join(command))
671 with open(os.devnull, "w") as dev_null:
672 subprocess.check_call(command, stderr=dev_null, stdout=dev_null)
673
674
675# pylint: disable=too-many-locals
676def AutoConnect(ip_addr, rsa_key_file, target_vnc_port, target_adb_port, ssh_user):
677 """Autoconnect to an AVD instance.
678
679 Args:
680 ip_addr: String, use to build the adb & vnc tunnel between local
681 and remote instance.
682 rsa_key_file: String, Private key file path to use when creating
683 the ssh tunnels.
684 target_vnc_port: Integer of target vnc port number.
685 target_adb_port: Integer of target adb port number.
686 ssh_user: String of user login into the instance.
687
688 Returns:
689 NamedTuple of (vnc_port, adb_port) SSHTUNNEL of the connect, both are
690 integers.
691 """
692 local_free_vnc_port = PickFreePort()
693 local_free_adb_port = PickFreePort()
694 try:
695 ssh_tunnel_args = _SSH_TUNNEL_ARGS % {
696 "rsa_key_file": rsa_key_file,
697 "vnc_port": local_free_vnc_port,
698 "adb_port": local_free_adb_port,
699 "target_vnc_port": target_vnc_port,
700 "target_adb_port": target_adb_port,
701 "ssh_user": ssh_user,
702 "ip_addr": ip_addr}
Kevin Cheng835a4152018-10-11 10:46:57 -0700703 _ExecuteCommand(constants.SSH_BIN, ssh_tunnel_args.split())
cylan66713722018-10-06 01:38:26 +0800704 except subprocess.CalledProcessError:
705 PrintColorString("Failed to create ssh tunnels, retry with '#acloud "
706 "reconnect'.", TextColors.FAIL)
707 try:
708 adb_connect_args = _ADB_CONNECT_ARGS % {"adb_port": local_free_adb_port}
Kevin Cheng835a4152018-10-11 10:46:57 -0700709 _ExecuteCommand(constants.ADB_BIN, adb_connect_args.split())
cylan66713722018-10-06 01:38:26 +0800710 except subprocess.CalledProcessError:
711 PrintColorString("Failed to adb connect, retry with "
712 "'#acloud reconnect'", TextColors.FAIL)
713
714 return ForwardedPorts(vnc_port=local_free_vnc_port,
715 adb_port=local_free_adb_port)
Kevin Chengeb85e862018-10-09 15:35:13 -0700716
717
718def GetAnswerFromList(answer_list, enable_choose_all=False):
719 """Get answer from a list.
720
721 Args:
722 answer_list: list of the answers to choose from.
723
724 Return:
725 List holding the answer(s).
726 """
727 print("[0] to exit.")
728 start_index = 1
729 if enable_choose_all:
730 start_index = 2
731 print("[1] for all.")
732 for num, item in enumerate(answer_list, start_index):
733 print("[%d] %s" % (num, item))
734
735 choice = -1
736 max_choice = len(answer_list) + 1
737 while True:
738 try:
739 choice = raw_input("Enter your choice[0-%d]: " % max_choice)
740 choice = int(choice)
741 except ValueError:
742 print("'%s' is not a valid integer.", choice)
743 continue
744 # Filter out choices
745 if choice == 0:
746 print("Exiting acloud.")
747 sys.exit()
748 if enable_choose_all and choice == 1:
749 return answer_list
750 if choice < 0 or choice > max_choice:
751 print("please choose between 0 and %d" % max_choice)
752 else:
753 return [answer_list[choice-start_index]]