1# Copyright 2016 - The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Common Utilities.""" 15# pylint: disable=too-many-lines 16from __future__ import print_function 17 18import base64 19import binascii 20import collections 21import errno 22import getpass 23import grp 24import logging 25import os 26import platform 27import re 28import shlex 29import shutil 30import signal 31import struct 32import socket 33import stat 34import subprocess 35import sys 36import tarfile 37import tempfile 38import time 39import uuid 40import webbrowser 41import zipfile 42 43from acloud import errors 44from acloud.internal import constants 45 46 47logger = logging.getLogger(__name__) 48 49SSH_KEYGEN_CMD = ["ssh-keygen", "-t", "rsa", "-b", "4096"] 50SSH_KEYGEN_PUB_CMD = ["ssh-keygen", "-y"] 51SSH_ARGS = ["-o", "UserKnownHostsFile=/dev/null", 52 "-o", "StrictHostKeyChecking=no"] 53SSH_CMD = ["ssh"] + SSH_ARGS 54SCP_CMD = ["scp"] + SSH_ARGS 55GET_BUILD_VAR_CMD = ["build/soong/soong_ui.bash", "--dumpvar-mode"] 56DEFAULT_RETRY_BACKOFF_FACTOR = 1 57DEFAULT_SLEEP_MULTIPLIER = 0 58 59_SSH_TUNNEL_ARGS = ( 60 "-i %(rsa_key_file)s -o UserKnownHostsFile=/dev/null " 61 "-o StrictHostKeyChecking=no " 62 "%(port_mapping)s" 63 "-N -f -l %(ssh_user)s %(ip_addr)s") 64_SSH_COMMAND_PS = ( 65 "exec %(ssh_bin)s -i %(rsa_key_file)s -o UserKnownHostsFile=/dev/null " 66 "-o StrictHostKeyChecking=no %(extra_args)s -l %(ssh_user)s %(ip_addr)s " 67 "ps aux") 68PORT_MAPPING = "-L %(local_port)d:127.0.0.1:%(target_port)d " 69_RELEASE_PORT_CMD = "kill $(lsof -t -i :%d)" 70_WEBRTC_OPERATOR_PATTERN = re.compile(r"(.+)(webrtc_operator )(.+)") 71_PORT_8443 = 8443 72_PORT_1443 = 1443 73PortMapping = collections.namedtuple("PortMapping", ["local", "target"]) 74WEBRTC_PORTS_MAPPING = [PortMapping(15550, 15550), 75 PortMapping(15551, 15551), 76 PortMapping(15552, 15552)] 77_RE_GROUP_WEBRTC = "local_webrtc_port" 78_RE_WEBRTC_SSH_TUNNEL_PATTERN = ( 79 r"((.*-L\s)(?P<local_webrtc_port>\d+):127.0.0.1:%s)(.+%s)") 80_ADB_CONNECT_ARGS = "connect 127.0.0.1:%(adb_port)d" 81# Store the ports that vnc/adb are forwarded to, both are integers. 82ForwardedPorts = collections.namedtuple("ForwardedPorts", [constants.VNC_PORT, 83 constants.ADB_PORT]) 84 85AVD_PORT_DICT = { 86 constants.TYPE_GCE: ForwardedPorts(constants.GCE_VNC_PORT, 87 constants.GCE_ADB_PORT), 88 constants.TYPE_CF: ForwardedPorts(constants.CF_VNC_PORT, 89 constants.CF_ADB_PORT), 90 constants.TYPE_GF: ForwardedPorts(constants.GF_VNC_PORT, 91 constants.GF_ADB_PORT), 92 constants.TYPE_CHEEPS: ForwardedPorts(constants.CHEEPS_VNC_PORT, 93 constants.CHEEPS_ADB_PORT), 94 constants.TYPE_FVP: ForwardedPorts(None, constants.FVP_ADB_PORT), 95} 96 97_VNC_BIN = "ssvnc" 98_CMD_KILL = ["pkill", "-9", "-f"] 99_CMD_SG = "sg " 100_CMD_START_VNC = "%(bin)s vnc://127.0.0.1:%(port)d" 101_CMD_INSTALL_SSVNC = "sudo apt-get --assume-yes install ssvnc" 102_ENV_DISPLAY = "DISPLAY" 103_SSVNC_ENV_VARS = {"SSVNC_NO_ENC_WARN": "1", "SSVNC_SCALE": "auto", "VNCVIEWER_X11CURSOR": "1"} 104_DEFAULT_DISPLAY_SCALE = 1.0 105_DIST_DIR = "DIST_DIR" 106 107# For webrtc 108_WEBRTC_URL = "https://%(webrtc_ip)s:%(webrtc_port)d" 109 110_CONFIRM_CONTINUE = ("In order to display the screen to the AVD, we'll need to " 111 "install a vnc client (ssvnc). \nWould you like acloud to " 112 "install it for you? (%s) \nPress 'y' to continue or " 113 "anything else to abort it[y/N]: ") % _CMD_INSTALL_SSVNC 114_EvaluatedResult = collections.namedtuple("EvaluatedResult", 115 ["is_result_ok", "result_message"]) 116# dict of supported system and their distributions. 117_SUPPORTED_SYSTEMS_AND_DISTS = {"Linux": ["Ubuntu", "ubuntu", "Debian", "debian"]} 118_DEFAULT_TIMEOUT_ERR = "Function did not complete within %d secs." 119_SSVNC_VIEWER_PATTERN = "vnc://127.0.0.1:%(vnc_port)d" 120 121# Determine the environment whether to support kvm. 122_KVM_PATH = "/dev/kvm" 123 124 125class TempDir: 126 """A context manager that ceates a temporary directory. 127 128 Attributes: 129 path: The path of the temporary directory. 130 """ 131 132 def __init__(self): 133 self.path = tempfile.mkdtemp() 134 os.chmod(self.path, 0o700) 135 logger.debug("Created temporary dir %s", self.path) 136 137 def __enter__(self): 138 """Enter.""" 139 return self.path 140 141 def __exit__(self, exc_type, exc_value, traceback): 142 """Exit. 143 144 Args: 145 exc_type: Exception type raised within the context manager. 146 None if no execption is raised. 147 exc_value: Exception instance raised within the context manager. 148 None if no execption is raised. 149 traceback: Traceback for exeception that is raised within 150 the context manager. 151 None if no execption is raised. 152 Raises: 153 EnvironmentError or OSError when failed to delete temp directory. 154 """ 155 try: 156 if self.path: 157 shutil.rmtree(self.path) 158 logger.debug("Deleted temporary dir %s", self.path) 159 except EnvironmentError as e: 160 # Ignore error if there is no exception raised 161 # within the with-clause and the EnvironementError is 162 # about problem that directory or file does not exist. 163 if not exc_type and e.errno != errno.ENOENT: 164 raise 165 except Exception as e: # pylint: disable=W0703 166 if exc_type: 167 logger.error( 168 "Encountered error while deleting %s: %s", 169 self.path, 170 str(e), 171 exc_info=True) 172 else: 173 raise 174 175 176def RetryOnException(retry_checker, 177 max_retries, 178 sleep_multiplier=0, 179 retry_backoff_factor=1): 180 """Decorater which retries the function call if |retry_checker| returns true. 181 182 Args: 183 retry_checker: A callback function which should take an exception instance 184 and return True if functor(*args, **kwargs) should be retried 185 when such exception is raised, and return False if it should 186 not be retried. 187 max_retries: Maximum number of retries allowed. 188 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if 189 retry_backoff_factor is 1. Will sleep 190 sleep_multiplier * ( 191 retry_backoff_factor ** (attempt_count - 1)) 192 if retry_backoff_factor != 1. 193 retry_backoff_factor: See explanation of sleep_multiplier. 194 195 Returns: 196 The function wrapper. 197 """ 198 199 def _Wrapper(func): 200 def _FunctionWrapper(*args, **kwargs): 201 return Retry(retry_checker, max_retries, func, sleep_multiplier, 202 retry_backoff_factor, *args, **kwargs) 203 204 return _FunctionWrapper 205 206 return _Wrapper 207 208 209def Retry(retry_checker, max_retries, functor, sleep_multiplier, 210 retry_backoff_factor, *args, **kwargs): 211 """Conditionally retry a function. 212 213 Args: 214 retry_checker: A callback function which should take an exception instance 215 and return True if functor(*args, **kwargs) should be retried 216 when such exception is raised, and return False if it should 217 not be retried. 218 max_retries: Maximum number of retries allowed. 219 functor: The function to call, will call functor(*args, **kwargs). 220 sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if 221 retry_backoff_factor is 1. Will sleep 222 sleep_multiplier * ( 223 retry_backoff_factor ** (attempt_count - 1)) 224 if retry_backoff_factor != 1. 225 retry_backoff_factor: See explanation of sleep_multiplier. 226 *args: Arguments to pass to the functor. 227 **kwargs: Key-val based arguments to pass to the functor. 228 229 Returns: 230 The return value of the functor. 231 232 Raises: 233 Exception: The exception that functor(*args, **kwargs) throws. 234 """ 235 attempt_count = 0 236 while attempt_count <= max_retries: 237 try: 238 attempt_count += 1 239 return_value = functor(*args, **kwargs) 240 return return_value 241 except Exception as e: # pylint: disable=W0703 242 if retry_checker(e) and attempt_count <= max_retries: 243 if retry_backoff_factor != 1: 244 sleep = sleep_multiplier * (retry_backoff_factor** 245 (attempt_count - 1)) 246 else: 247 sleep = sleep_multiplier * attempt_count 248 time.sleep(sleep) 249 else: 250 raise 251 252 253def RetryExceptionType(exception_types, max_retries, functor, *args, **kwargs): 254 """Retry exception if it is one of the given types. 255 256 Args: 257 exception_types: A tuple of exception types, e.g. (ValueError, KeyError) 258 max_retries: Max number of retries allowed. 259 functor: The function to call. Will be retried if exception is raised and 260 the exception is one of the exception_types. 261 *args: Arguments to pass to Retry function. 262 **kwargs: Key-val based arguments to pass to Retry functions. 263 264 Returns: 265 The value returned by calling functor. 266 """ 267 return Retry(lambda e: isinstance(e, exception_types), max_retries, 268 functor, *args, **kwargs) 269 270 271def PollAndWait(func, expected_return, timeout_exception, timeout_secs, 272 sleep_interval_secs, *args, **kwargs): 273 """Call a function until the function returns expected value or times out. 274 275 Args: 276 func: Function to call. 277 expected_return: The expected return value. 278 timeout_exception: Exception to raise when it hits timeout. 279 timeout_secs: Timeout seconds. 280 If 0 or less than zero, the function will run once and 281 we will not wait on it. 282 sleep_interval_secs: Time to sleep between two attemps. 283 *args: list of args to pass to func. 284 **kwargs: dictionary of keyword based args to pass to func. 285 286 Raises: 287 timeout_exception: if the run of function times out. 288 """ 289 # TODO(fdeng): Currently this method does not kill 290 # |func|, if |func| takes longer than |timeout_secs|. 291 # We can use a more robust version from chromite. 292 start = time.time() 293 while True: 294 return_value = func(*args, **kwargs) 295 if return_value == expected_return: 296 return 297 if time.time() - start > timeout_secs: 298 raise timeout_exception 299 if sleep_interval_secs > 0: 300 time.sleep(sleep_interval_secs) 301 302 303def GenerateUniqueName(prefix=None, suffix=None): 304 """Generate a random unique name using uuid4. 305 306 Args: 307 prefix: String, desired prefix to prepend to the generated name. 308 suffix: String, desired suffix to append to the generated name. 309 310 Returns: 311 String, a random name. 312 """ 313 name = uuid.uuid4().hex 314 if prefix: 315 name = "-".join([prefix, name]) 316 if suffix: 317 name = "-".join([name, suffix]) 318 return name 319 320 321def MakeTarFile(src_dict, dest): 322 """Archive files in tar.gz format to a file named as |dest|. 323 324 Args: 325 src_dict: A dictionary that maps a path to be archived 326 to the corresponding name that appears in the archive. 327 dest: String, path to output file, e.g. /tmp/myfile.tar.gz 328 """ 329 logger.info("Compressing %s into %s.", src_dict.keys(), dest) 330 with tarfile.open(dest, "w:gz") as tar: 331 for src, arcname in src_dict.items(): 332 tar.add(src, arcname=arcname) 333 334def CreateSshKeyPairIfNotExist(private_key_path, public_key_path): 335 """Create the ssh key pair if they don't exist. 336 337 Case1. If the private key doesn't exist, we will create both the public key 338 and the private key. 339 Case2. If the private key exists but public key doesn't, we will create the 340 public key by using the private key. 341 Case3. If the public key exists but the private key doesn't, we will create 342 a new private key and overwrite the public key. 343 344 Args: 345 private_key_path: Path to the private key file. 346 e.g. ~/.ssh/acloud_rsa 347 public_key_path: Path to the public key file. 348 e.g. ~/.ssh/acloud_rsa.pub 349 350 Raises: 351 error.DriverError: If failed to create the key pair. 352 """ 353 public_key_path = os.path.expanduser(public_key_path) 354 private_key_path = os.path.expanduser(private_key_path) 355 public_key_exist = os.path.exists(public_key_path) 356 private_key_exist = os.path.exists(private_key_path) 357 if public_key_exist and private_key_exist: 358 logger.debug( 359 "The ssh private key (%s) and public key (%s) already exist," 360 "will not automatically create the key pairs.", private_key_path, 361 public_key_path) 362 return 363 key_folder = os.path.dirname(private_key_path) 364 if not os.path.exists(key_folder): 365 os.makedirs(key_folder) 366 try: 367 if private_key_exist: 368 cmd = SSH_KEYGEN_PUB_CMD + ["-f", private_key_path] 369 with open(public_key_path, 'w') as outfile: 370 stream_content = CheckOutput(cmd) 371 outfile.write( 372 stream_content.rstrip('\n') + " " + getpass.getuser()) 373 logger.info( 374 "The ssh public key (%s) do not exist, " 375 "automatically creating public key, calling: %s", 376 public_key_path, " ".join(cmd)) 377 else: 378 cmd = SSH_KEYGEN_CMD + [ 379 "-C", getpass.getuser(), "-f", private_key_path 380 ] 381 logger.info( 382 "Creating public key from private key (%s) via cmd: %s", 383 private_key_path, " ".join(cmd)) 384 subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout) 385 except subprocess.CalledProcessError as e: 386 raise errors.DriverError("Failed to create ssh key pair: %s" % str(e)) 387 except OSError as e: 388 raise errors.DriverError( 389 "Failed to create ssh key pair, please make sure " 390 "'ssh-keygen' is installed: %s" % str(e)) 391 392 # By default ssh-keygen will create a public key file 393 # by append .pub to the private key file name. Rename it 394 # to what's requested by public_key_path. 395 default_pub_key_path = "%s.pub" % private_key_path 396 try: 397 if default_pub_key_path != public_key_path: 398 os.rename(default_pub_key_path, public_key_path) 399 except OSError as e: 400 raise errors.DriverError( 401 "Failed to rename %s to %s: %s" % (default_pub_key_path, 402 public_key_path, str(e))) 403 404 logger.info("Created ssh private key (%s) and public key (%s)", 405 private_key_path, public_key_path) 406 407 408def VerifyRsaPubKey(rsa): 409 """Verify the format of rsa public key. 410 411 Args: 412 rsa: content of rsa public key. It should follow the format of 413 ssh-rsa AAAAB3NzaC1yc2EA.... test@test.com 414 415 Raises: 416 DriverError if the format is not correct. 417 """ 418 if not rsa or not all(ord(c) < 128 for c in rsa): 419 raise errors.DriverError( 420 "rsa key is empty or contains non-ascii character: %s" % rsa) 421 422 elements = rsa.split() 423 if len(elements) != 3: 424 raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa) 425 426 key_type, data, _ = elements 427 try: 428 binary_data = base64.decodebytes(data.encode()) 429 # number of bytes of int type 430 int_length = 4 431 # binary_data is like "7ssh-key..." in a binary format. 432 # The first 4 bytes should represent 7, which should be 433 # the length of the following string "ssh-key". 434 # And the next 7 bytes should be string "ssh-key". 435 # We will verify that the rsa conforms to this format. 436 # ">I" in the following line means "big-endian unsigned integer". 437 type_length = struct.unpack(">I", binary_data[:int_length])[0] 438 if binary_data[int_length:int_length + type_length] != key_type.encode(): 439 raise errors.DriverError("rsa key is invalid: %s" % rsa) 440 except (struct.error, binascii.Error) as e: 441 raise errors.DriverError( 442 "rsa key is invalid: %s, error: %s" % (rsa, str(e))) 443 444 445def Decompress(sourcefile, dest=None): 446 """Decompress .zip or .tar.gz. 447 448 Args: 449 sourcefile: A string, a source file path to decompress. 450 dest: A string, a folder path as decompress destination. 451 452 Raises: 453 errors.UnsupportedCompressionFileType: Not supported extension. 454 """ 455 logger.info("Start to decompress %s!", sourcefile) 456 dest_path = dest if dest else "." 457 if sourcefile.endswith(".tar.gz"): 458 with tarfile.open(sourcefile, "r:gz") as compressor: 459 compressor.extractall(dest_path) 460 elif sourcefile.endswith(".zip"): 461 with zipfile.ZipFile(sourcefile, 'r') as compressor: 462 compressor.extractall(dest_path) 463 else: 464 raise errors.UnsupportedCompressionFileType( 465 "Sorry, we could only support compression file type " 466 "for zip or tar.gz.") 467 468 469# pylint: disable=no-init 470class TextColors: 471 """A class that defines common color ANSI code.""" 472 473 HEADER = "\033[95m" 474 OKBLUE = "\033[94m" 475 OKGREEN = "\033[92m" 476 WARNING = "\033[33m" 477 FAIL = "\033[91m" 478 ENDC = "\033[0m" 479 BOLD = "\033[1m" 480 UNDERLINE = "\033[4m" 481 482 483def PrintColorString(message, colors=TextColors.OKBLUE, **kwargs): 484 """A helper function to print out colored text. 485 486 Use print function "print(message, end="")" to show message in one line. 487 Example code: 488 DisplayMessages("Creating GCE instance...", end="") 489 # Job execute 20s 490 DisplayMessages("Done! (20s)") 491 Display: 492 Creating GCE instance... 493 # After job finished, messages update as following: 494 Creating GCE instance...Done! (20s) 495 496 Args: 497 message: String, the message text. 498 colors: String, color code. 499 **kwargs: dictionary of keyword based args to pass to func. 500 """ 501 print(colors + message + TextColors.ENDC, **kwargs) 502 sys.stdout.flush() 503 504 505def InteractWithQuestion(question, colors=TextColors.WARNING): 506 """A helper function to define the common way to run interactive cmd. 507 508 Args: 509 question: String, the question to ask user. 510 colors: String, color code. 511 512 Returns: 513 String, input from user. 514 """ 515 return str(input(colors + question + TextColors.ENDC).strip()) 516 517 518def GetUserAnswerYes(question): 519 """Ask user about acloud setup question. 520 521 Args: 522 question: String of question for user. Enter is equivalent to pressing 523 n. We should hint user with upper case N surrounded in square 524 brackets. 525 Ex: "Are you sure to change bucket name[y/N]:" 526 527 Returns: 528 Boolean, True if answer is "Yes", False otherwise. 529 """ 530 answer = InteractWithQuestion(question) 531 return answer.lower() in constants.USER_ANSWER_YES 532 533 534class BatchHttpRequestExecutor: 535 """A helper class that executes requests in batch with retry. 536 537 This executor executes http requests in a batch and retry 538 those that have failed. It iteratively updates the dictionary 539 self._final_results with latest results, which can be retrieved 540 via GetResults. 541 """ 542 543 def __init__(self, 544 execute_once_functor, 545 requests, 546 retry_http_codes=None, 547 max_retry=None, 548 sleep=None, 549 backoff_factor=None, 550 other_retriable_errors=None): 551 """Initializes the executor. 552 553 Args: 554 execute_once_functor: A function that execute requests in batch once. 555 It should return a dictionary like 556 {request_id: (response, exception)} 557 requests: A dictionary where key is request id picked by caller, 558 and value is a apiclient.http.HttpRequest. 559 retry_http_codes: A list of http codes to retry. 560 max_retry: See utils.Retry. 561 sleep: See utils.Retry. 562 backoff_factor: See utils.Retry. 563 other_retriable_errors: A tuple of error types that should be retried 564 other than errors.HttpError. 565 """ 566 self._execute_once_functor = execute_once_functor 567 self._requests = requests 568 # A dictionary that maps request id to pending request. 569 self._pending_requests = {} 570 # A dictionary that maps request id to a tuple (response, exception). 571 self._final_results = {} 572 self._retry_http_codes = retry_http_codes 573 self._max_retry = max_retry 574 self._sleep = sleep 575 self._backoff_factor = backoff_factor 576 self._other_retriable_errors = other_retriable_errors 577 578 def _ShoudRetry(self, exception): 579 """Check if an exception is retriable. 580 581 Args: 582 exception: An exception instance. 583 """ 584 if isinstance(exception, self._other_retriable_errors): 585 return True 586 587 if (isinstance(exception, errors.HttpError) 588 and exception.code in self._retry_http_codes): 589 return True 590 return False 591 592 def _ExecuteOnce(self): 593 """Executes pending requests and update it with failed, retriable ones. 594 595 Raises: 596 HasRetriableRequestsError: if some requests fail and are retriable. 597 """ 598 results = self._execute_once_functor(self._pending_requests) 599 # Update final_results with latest results. 600 self._final_results.update(results) 601 # Clear pending_requests 602 self._pending_requests.clear() 603 for request_id, result in results.items(): 604 exception = result[1] 605 if exception is not None and self._ShoudRetry(exception): 606 # If this is a retriable exception, put it in pending_requests 607 self._pending_requests[request_id] = self._requests[request_id] 608 if self._pending_requests: 609 # If there is still retriable requests pending, raise an error 610 # so that Retry will retry this function with pending_requests. 611 raise errors.HasRetriableRequestsError( 612 "Retriable errors: %s" % 613 [str(results[rid][1]) for rid in self._pending_requests]) 614 615 def Execute(self): 616 """Executes the requests and retry if necessary. 617 618 Will populate self._final_results. 619 """ 620 621 def _ShouldRetryHandler(exc): 622 """Check if |exc| is a retriable exception. 623 624 Args: 625 exc: An exception. 626 627 Returns: 628 True if exception is of type HasRetriableRequestsError; False otherwise. 629 """ 630 should_retry = isinstance(exc, errors.HasRetriableRequestsError) 631 if should_retry: 632 logger.info("Will retry failed requests.", exc_info=True) 633 logger.info("%s", exc) 634 return should_retry 635 636 try: 637 self._pending_requests = self._requests.copy() 638 Retry( 639 _ShouldRetryHandler, 640 max_retries=self._max_retry, 641 functor=self._ExecuteOnce, 642 sleep_multiplier=self._sleep, 643 retry_backoff_factor=self._backoff_factor) 644 except errors.HasRetriableRequestsError: 645 logger.debug("Some requests did not succeed after retry.") 646 647 def GetResults(self): 648 """Returns final results. 649 650 Returns: 651 results, a dictionary in the following format 652 {request_id: (response, exception)} 653 request_ids are those from requests; response 654 is the http response for the request or None on error; 655 exception is an instance of DriverError or None if no error. 656 """ 657 return self._final_results 658 659 660def DefaultEvaluator(result): 661 """Default Evaluator always return result is ok. 662 663 Args: 664 result:the return value of the target function. 665 666 Returns: 667 _EvaluatedResults namedtuple. 668 """ 669 return _EvaluatedResult(is_result_ok=True, result_message=result) 670 671 672def ReportEvaluator(report): 673 """Evalute the acloud operation by the report. 674 675 Args: 676 report: acloud.public.report() object. 677 678 Returns: 679 _EvaluatedResults namedtuple. 680 """ 681 if report is None or report.errors: 682 return _EvaluatedResult(is_result_ok=False, 683 result_message=report.errors) 684 685 return _EvaluatedResult(is_result_ok=True, result_message=None) 686 687 688def BootEvaluator(boot_dict): 689 """Evaluate if the device booted successfully. 690 691 Args: 692 boot_dict: Dict of instance_name:boot error. 693 694 Returns: 695 _EvaluatedResults namedtuple. 696 """ 697 if boot_dict: 698 return _EvaluatedResult(is_result_ok=False, result_message=boot_dict) 699 return _EvaluatedResult(is_result_ok=True, result_message=None) 700 701 702class TimeExecute: 703 """Count the function execute time.""" 704 705 def __init__(self, function_description=None, print_before_call=True, 706 print_status=True, result_evaluator=DefaultEvaluator, 707 display_waiting_dots=True): 708 """Initializes the class. 709 710 Args: 711 function_description: String that describes function (e.g."Creating 712 Instance...") 713 print_before_call: Boolean, print the function description before 714 calling the function, default True. 715 print_status: Boolean, print the status of the function after the 716 function has completed, default True ("OK" or "Fail"). 717 result_evaluator: Func object. Pass func to evaluate result. 718 Default evaluator always report result is ok and 719 failed result will be identified only in exception 720 case. 721 display_waiting_dots: Boolean, if true print the function_description 722 followed by waiting dot. 723 """ 724 self._function_description = function_description 725 self._print_before_call = print_before_call 726 self._print_status = print_status 727 self._result_evaluator = result_evaluator 728 self._display_waiting_dots = display_waiting_dots 729 730 def __call__(self, func): 731 def DecoratorFunction(*args, **kargs): 732 """Decorator function. 733 734 Args: 735 *args: Arguments to pass to the functor. 736 **kwargs: Key-val based arguments to pass to the functor. 737 738 Raises: 739 Exception: The exception that functor(*args, **kwargs) throws. 740 """ 741 timestart = time.time() 742 if self._print_before_call: 743 waiting_dots = "..." if self._display_waiting_dots else "" 744 PrintColorString("%s %s"% (self._function_description, 745 waiting_dots), end="") 746 try: 747 result = func(*args, **kargs) 748 result_time = time.time() - timestart 749 if not self._print_before_call: 750 PrintColorString("%s (%ds)" % (self._function_description, 751 result_time), 752 TextColors.OKGREEN) 753 if self._print_status: 754 evaluated_result = self._result_evaluator(result) 755 if evaluated_result.is_result_ok: 756 PrintColorString("OK! (%ds)" % (result_time), 757 TextColors.OKGREEN) 758 else: 759 PrintColorString("Fail! (%ds)" % (result_time), 760 TextColors.FAIL) 761 PrintColorString("Error: %s" % 762 evaluated_result.result_message, 763 TextColors.FAIL) 764 return result 765 except: 766 if self._print_status: 767 PrintColorString("Fail! (%ds)" % (time.time() - timestart), 768 TextColors.FAIL) 769 raise 770 return DecoratorFunction 771 772 773def PickFreePort(): 774 """Helper to pick a free port. 775 776 Returns: 777 Integer, a free port number. 778 """ 779 tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 780 tcp_socket.bind(("", 0)) 781 port = tcp_socket.getsockname()[1] 782 tcp_socket.close() 783 return port 784 785 786def CheckPortFree(port): 787 """Check the availablity of the tcp port. 788 789 Args: 790 Integer, a port number. 791 792 Raises: 793 PortOccupied: This port is not available. 794 """ 795 tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 796 try: 797 tcp_socket.bind(("", port)) 798 except socket.error as port_error: 799 raise errors.PortOccupied("Port (%d) is taken, please choose another " 800 "port." % port) from port_error 801 tcp_socket.close() 802 803 804def _ExecuteCommand(cmd, args): 805 """Execute command. 806 807 Args: 808 cmd: Strings of execute binary name. 809 args: List of args to pass in with cmd. 810 811 Raises: 812 errors.NoExecuteBin: Can't find the execute bin file. 813 """ 814 bin_path = FindExecutable(cmd) 815 if not bin_path: 816 raise errors.NoExecuteCmd("unable to locate %s" % cmd) 817 command = [bin_path] + args 818 logger.debug("Running '%s'", ' '.join(command)) 819 with open(os.devnull, "w") as dev_null: 820 subprocess.check_call(command, stderr=dev_null, stdout=dev_null) 821 822 823def ReleasePort(port): 824 """Release local port. 825 826 Args: 827 port: Integer of local port number. 828 """ 829 try: 830 with open(os.devnull, "w") as dev_null: 831 subprocess.check_call(_RELEASE_PORT_CMD % port, 832 stderr=dev_null, stdout=dev_null, shell=True) 833 except subprocess.CalledProcessError: 834 logger.debug("The port %d is available.", constants.WEBRTC_LOCAL_PORT) 835 836 837def EstablishSshTunnel(ip_addr, rsa_key_file, ssh_user, 838 port_mapping, extra_args_ssh_tunnel=None): 839 """Create an ssh tunnel. 840 841 Args: 842 ip_addr: String, use to build the adb & vnc tunnel between local 843 and remote instance. 844 rsa_key_file: String, Private key file path to use when creating 845 the ssh tunnels. 846 ssh_user: String of user login into the instance. 847 port_mapping: List of tuples, each tuple is a pair of integers 848 representing a local port and a remote port. 849 extra_args_ssh_tunnel: String, extra args for ssh tunnel connection. 850 851 Raises: 852 subprocess.CalledProcessError if the ssh command fails. 853 """ 854 port_mapping = [PORT_MAPPING % { 855 "local_port": ports[0], 856 "target_port": ports[1]} for ports in port_mapping] 857 ssh_tunnel_args = _SSH_TUNNEL_ARGS % { 858 "rsa_key_file": rsa_key_file, 859 "ssh_user": ssh_user, 860 "ip_addr": ip_addr, 861 "port_mapping": " ".join(port_mapping)} 862 ssh_tunnel_args_list = shlex.split(ssh_tunnel_args) 863 if extra_args_ssh_tunnel: 864 ssh_tunnel_args_list.extend(shlex.split(extra_args_ssh_tunnel)) 865 _ExecuteCommand(constants.SSH_BIN, ssh_tunnel_args_list) 866 867 868def EstablishWebRTCSshTunnel(ip_addr, webrtc_local_port, rsa_key_file, ssh_user, 869 extra_args_ssh_tunnel=None): 870 """Create ssh tunnels for webrtc. 871 872 Pick up an available local port to establish one WebRTC tunnel and forward to 873 the port of the webrtc operator of the remote instance. 874 875 Args: 876 ip_addr: String, use to build the adb & vnc tunnel between local 877 and remote instance. 878 webrtc_local_port: Integer, pick a free port as webrtc local port. 879 rsa_key_file: String, Private key file path to use when creating 880 the ssh tunnels. 881 ssh_user: String of user login into the instance. 882 extra_args_ssh_tunnel: String, extra args for ssh tunnel connection. 883 884 Raises: 885 subprocess.CalledProcessError if the ssh command fails. 886 """ 887 webrtc_server_port = GetWebRTCServerPort( 888 ip_addr, rsa_key_file, ssh_user, extra_args_ssh_tunnel) 889 890 # TODO(b/209502647): design a better way to forward webrtc ports. 891 if extra_args_ssh_tunnel: 892 for webrtc_port in WEBRTC_PORTS_MAPPING: 893 ReleasePort(webrtc_port.local) 894 port_mapping = (WEBRTC_PORTS_MAPPING + 895 [PortMapping(webrtc_local_port, webrtc_server_port)]) 896 try: 897 EstablishSshTunnel(ip_addr, rsa_key_file, ssh_user, 898 port_mapping, extra_args_ssh_tunnel) 899 except subprocess.CalledProcessError as e: 900 PrintColorString("\n%s\nFailed to create ssh tunnels, retry with '#acloud " 901 "reconnect'." % e, TextColors.FAIL) 902 903 904def GetWebRTCServerPort(ip_addr, rsa_key_file, ssh_user, 905 extra_args_ssh_tunnel=None): 906 """Get WebRTC server port. 907 908 List all process information to find the "webrtc_operator" process, then 909 determine the WebRTC server port is 8443 or 1443. 910 911 Args: 912 ip_addr: String, use to build the adb & vnc tunnel between local 913 and remote instance. 914 rsa_key_file: String, Private key file path to use when creating 915 the ssh tunnels. 916 ssh_user: String of user login into the instance. 917 extra_args_ssh_tunnel: String, extra args for ssh tunnel connection. 918 919 Returns: 920 The WebRTC server port number. 921 922 Raises: 923 subprocess.CalledProcessError if the ssh command fails. 924 """ 925 ssh_cmd = _SSH_COMMAND_PS % { 926 "ssh_bin": FindExecutable(constants.SSH_BIN), 927 "rsa_key_file": rsa_key_file, 928 "ssh_user": ssh_user, 929 "extra_args": extra_args_ssh_tunnel or "", 930 "ip_addr": ip_addr} 931 logger.info("Running command \"%s\"", ssh_cmd) 932 try: 933 process = subprocess.Popen( 934 ssh_cmd, shell=True, stdin=None, universal_newlines=True, 935 stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 936 stdout, _ = process.communicate() 937 for line in stdout.splitlines(): 938 webrtc_match = _WEBRTC_OPERATOR_PATTERN.match(line) 939 if webrtc_match: 940 return _PORT_8443 941 except subprocess.CalledProcessError as e: 942 logger.debug("Failed to list processes: %s", e) 943 return _PORT_1443 944 945 946def GetWebrtcPortFromSSHTunnel(ip): 947 """Get forwarding webrtc port from ssh tunnel. 948 949 Args: 950 ip: String, ip address. 951 952 Returns: 953 webrtc local port. 954 """ 955 re_pattern = re.compile(_RE_WEBRTC_SSH_TUNNEL_PATTERN % 956 (constants.WEBRTC_LOCAL_PORT, ip)) 957 process_output = CheckOutput(constants.COMMAND_PS) 958 for line in process_output.splitlines(): 959 match = re_pattern.match(line) 960 if match: 961 webrtc_port = int(match.group(_RE_GROUP_WEBRTC)) 962 return webrtc_port 963 964 logger.debug("Can't get webrtc local port from ip %s.", ip) 965 return None 966 967 968# TODO(147337696): create ssh tunnels tear down as adb and vnc. 969# pylint: disable=too-many-locals 970def AutoConnect(ip_addr, rsa_key_file, target_vnc_port, target_adb_port, 971 ssh_user, client_adb_port=None, extra_args_ssh_tunnel=None): 972 """Autoconnect to an AVD instance. 973 974 Args: 975 ip_addr: String, use to build the adb & vnc tunnel between local 976 and remote instance. 977 rsa_key_file: String, Private key file path to use when creating 978 the ssh tunnels. 979 target_vnc_port: Integer of target vnc port number. 980 target_adb_port: Integer of target adb port number. 981 ssh_user: String of user login into the instance. 982 client_adb_port: Integer, Specified adb port to establish connection. 983 extra_args_ssh_tunnel: String, extra args for ssh tunnel connection. 984 985 Returns: 986 NamedTuple of (vnc_port, adb_port) SSHTUNNEL of the connect, both are 987 integers. 988 """ 989 local_adb_port = client_adb_port or PickFreePort() 990 port_mapping = [(local_adb_port, target_adb_port)] 991 local_free_vnc_port = None 992 if target_vnc_port: 993 local_free_vnc_port = PickFreePort() 994 port_mapping.append((local_free_vnc_port, target_vnc_port)) 995 try: 996 EstablishSshTunnel(ip_addr, rsa_key_file, ssh_user, 997 port_mapping, extra_args_ssh_tunnel) 998 except subprocess.CalledProcessError as e: 999 PrintColorString("\n%s\nFailed to create ssh tunnels, retry with '#acloud " 1000 "reconnect'." % e, TextColors.FAIL) 1001 return ForwardedPorts(vnc_port=None, adb_port=None) 1002 1003 try: 1004 adb_connect_args = _ADB_CONNECT_ARGS % {"adb_port": local_adb_port} 1005 _ExecuteCommand(constants.ADB_BIN, adb_connect_args.split()) 1006 except subprocess.CalledProcessError: 1007 PrintColorString("Failed to adb connect, retry with " 1008 "'#acloud reconnect'", TextColors.FAIL) 1009 1010 return ForwardedPorts(vnc_port=local_free_vnc_port, 1011 adb_port=local_adb_port) 1012 1013 1014def GetAnswerFromList(answer_list, enable_choose_all=False): 1015 """Get answer from a list. 1016 1017 Args: 1018 answer_list: list of the answers to choose from. 1019 enable_choose_all: True to choose all items from answer list. 1020 1021 Return: 1022 List holding the answer(s). 1023 """ 1024 print("[0] to exit.") 1025 start_index = 1 1026 max_choice = len(answer_list) 1027 1028 for num, item in enumerate(answer_list, start_index): 1029 print("[%d] %s" % (num, item)) 1030 if enable_choose_all: 1031 max_choice += 1 1032 print("[%d] for all." % max_choice) 1033 1034 choice = -1 1035 1036 while True: 1037 try: 1038 choice = input("Enter your choice[0-%d]: " % max_choice) 1039 choice = int(choice) 1040 except ValueError: 1041 print("'%s' is not a valid integer.", choice) 1042 continue 1043 # Filter out choices 1044 if choice == 0: 1045 sys.exit(constants.EXIT_BY_USER) 1046 if enable_choose_all and choice == max_choice: 1047 return answer_list 1048 if choice < 0 or choice > max_choice: 1049 print("please choose between 0 and %d" % max_choice) 1050 else: 1051 return [answer_list[choice-start_index]] 1052 1053 1054def LaunchVNCFromReport(report, avd_spec, no_prompts=False): 1055 """Launch vnc client according to the instances report. 1056 1057 Args: 1058 report: Report object, that stores and generates report. 1059 avd_spec: AVDSpec object that tells us what we're going to create. 1060 no_prompts: Boolean, True to skip all prompts. 1061 """ 1062 for device in report.data.get("devices", []): 1063 if device.get(constants.VNC_PORT): 1064 LaunchVncClient(device.get(constants.VNC_PORT), 1065 avd_width=avd_spec.hw_property["x_res"], 1066 avd_height=avd_spec.hw_property["y_res"], 1067 no_prompts=no_prompts) 1068 else: 1069 PrintColorString("No VNC port specified, skipping VNC startup.", 1070 TextColors.FAIL) 1071 1072 1073def LaunchBrowserFromReport(report): 1074 """Open browser when autoconnect to webrtc according to the instances report. 1075 1076 Args: 1077 report: Report object, that stores and generates report. 1078 """ 1079 for device in report.data.get("devices", []): 1080 if device.get("ip"): 1081 LaunchBrowser(constants.WEBRTC_LOCAL_HOST, 1082 device.get(constants.WEBRTC_PORT, 1083 constants.WEBRTC_LOCAL_PORT)) 1084 1085 1086def LaunchBrowser(ip_addr, port): 1087 """Launch browser to connect the webrtc AVD. 1088 1089 Args: 1090 ip_addr: String, use to connect to webrtc AVD on the instance. 1091 port: Integer, port number. 1092 """ 1093 webrtc_link = _WEBRTC_URL % { 1094 "webrtc_ip": ip_addr, 1095 "webrtc_port": port} 1096 if os.environ.get(_ENV_DISPLAY, None): 1097 webbrowser.open_new_tab(webrtc_link) 1098 else: 1099 PrintColorString("Remote terminal can't support launch webbrowser.", 1100 TextColors.FAIL) 1101 PrintColorString("WebRTC AVD URL: %s "% webrtc_link) 1102 1103 1104def LaunchVncClient(port, avd_width=None, avd_height=None, no_prompts=False): 1105 """Launch ssvnc. 1106 1107 Args: 1108 port: Integer, port number. 1109 avd_width: String, the width of avd. 1110 avd_height: String, the height of avd. 1111 no_prompts: Boolean, True to skip all prompts. 1112 """ 1113 try: 1114 os.environ[_ENV_DISPLAY] 1115 except KeyError: 1116 PrintColorString("Remote terminal can't support VNC. " 1117 "Skipping VNC startup. " 1118 "VNC server is listening at 127.0.0.1:{}.".format(port), 1119 TextColors.FAIL) 1120 return 1121 1122 if IsSupportedPlatform() and not FindExecutable(_VNC_BIN): 1123 if no_prompts or GetUserAnswerYes(_CONFIRM_CONTINUE): 1124 try: 1125 PrintColorString("Installing ssvnc vnc client... ", end="") 1126 sys.stdout.flush() 1127 CheckOutput(_CMD_INSTALL_SSVNC, shell=True) 1128 PrintColorString("Done", TextColors.OKGREEN) 1129 except subprocess.CalledProcessError as cpe: 1130 PrintColorString("Failed to install ssvnc: %s" % 1131 cpe.output, TextColors.FAIL) 1132 return 1133 else: 1134 return 1135 ssvnc_env = os.environ.copy() 1136 ssvnc_env.update(_SSVNC_ENV_VARS) 1137 # Override SSVNC_SCALE 1138 if avd_width or avd_height: 1139 scale_ratio = CalculateVNCScreenRatio(avd_width, avd_height) 1140 ssvnc_env["SSVNC_SCALE"] = str(scale_ratio) 1141 logger.debug("SSVNC_SCALE:%s", scale_ratio) 1142 1143 ssvnc_args = _CMD_START_VNC % {"bin": FindExecutable(_VNC_BIN), 1144 "port": port} 1145 subprocess.Popen(ssvnc_args.split(), env=ssvnc_env) 1146 1147 1148def PrintDeviceSummary(report): 1149 """Display summary of devices. 1150 1151 -Display device details from the report instance. 1152 report example: 1153 'data': [{'devices':[{'instance_name': 'ins-f6a397-none-53363', 1154 'ip': u'35.234.10.162'}]}] 1155 -Display error message from report.error. 1156 1157 Args: 1158 report: A Report instance. 1159 """ 1160 PrintColorString("\n") 1161 PrintColorString("Device summary:") 1162 for device in report.data.get("devices", []): 1163 adb_serial = device.get(constants.DEVICE_SERIAL) 1164 if not adb_serial: 1165 adb_port = device.get("adb_port") 1166 if adb_port: 1167 adb_serial = constants.LOCALHOST_ADB_SERIAL % adb_port 1168 else: 1169 adb_serial = "(None)" 1170 1171 instance_name = device.get("instance_name") 1172 instance_ip = device.get("ip") 1173 instance_details = "" if not instance_name else "(%s[%s])" % ( 1174 instance_name, instance_ip) 1175 PrintColorString(f" - device serial: {adb_serial} {instance_details}") 1176 PrintColorString("\n") 1177 PrintColorString("Note: To ensure Tradefed uses this AVD, please run:") 1178 PrintColorString("\texport ANDROID_SERIAL=%s" % adb_serial) 1179 ssh_command = device.get("ssh_command") 1180 if ssh_command: 1181 PrintColorString("\n") 1182 PrintColorString("Note: To ssh connect to the device, please run:") 1183 PrintColorString(f"\tssh command: {ssh_command}") 1184 screen_command = device.get("screen_command") 1185 if screen_command: 1186 PrintColorString("\n") 1187 PrintColorString("Note: To access the console, please run:") 1188 PrintColorString(f"\tscreen command: {screen_command}") 1189 1190 # TODO(b/117245508): Help user to delete instance if it got created. 1191 if report.errors: 1192 error_msg = "\n".join(report.errors) 1193 PrintColorString("Fail in:\n%s\n" % error_msg, TextColors.FAIL) 1194 1195 1196# pylint: disable=import-outside-toplevel 1197def CalculateVNCScreenRatio(avd_width, avd_height): 1198 """calculate the vnc screen scale ratio to fit into user's monitor. 1199 1200 Args: 1201 avd_width: String, the width of avd. 1202 avd_height: String, the height of avd. 1203 Return: 1204 Float, scale ratio for vnc client. 1205 """ 1206 try: 1207 import Tkinter 1208 # Some python interpreters may not be configured for Tk, just return default scale ratio. 1209 except ImportError: 1210 try: 1211 import tkinter as Tkinter 1212 except ImportError: 1213 PrintColorString( 1214 "no module named tkinter, vnc display scale were not be fit." 1215 "please run 'sudo apt-get install python3-tk' to install it.") 1216 return _DEFAULT_DISPLAY_SCALE 1217 root = Tkinter.Tk() 1218 margin = 100 # leave some space on user's monitor. 1219 screen_height = root.winfo_screenheight() - margin 1220 screen_width = root.winfo_screenwidth() - margin 1221 1222 scale_h = _DEFAULT_DISPLAY_SCALE 1223 scale_w = _DEFAULT_DISPLAY_SCALE 1224 if float(screen_height) < float(avd_height): 1225 scale_h = round(float(screen_height) / float(avd_height), 1) 1226 1227 if float(screen_width) < float(avd_width): 1228 scale_w = round(float(screen_width) / float(avd_width), 1) 1229 1230 logger.debug("scale_h: %s (screen_h: %s/avd_h: %s)," 1231 " scale_w: %s (screen_w: %s/avd_w: %s)", 1232 scale_h, screen_height, avd_height, 1233 scale_w, screen_width, avd_width) 1234 1235 # Return the larger scale-down ratio. 1236 return scale_h if scale_h < scale_w else scale_w 1237 1238 1239def IsCommandRunning(command): 1240 """Check if command is running. 1241 1242 Args: 1243 command: String of command name. 1244 1245 Returns: 1246 Boolean, True if command is running. False otherwise. 1247 """ 1248 try: 1249 with open(os.devnull, "w") as dev_null: 1250 subprocess.check_call([constants.CMD_PGREP, "-af", command], 1251 stderr=dev_null, stdout=dev_null) 1252 return True 1253 except subprocess.CalledProcessError: 1254 return False 1255 1256 1257def AddUserGroupsToCmd(cmd, user_groups): 1258 """Add the user groups to the command if necessary. 1259 1260 As part of local host setup to enable local instance support, the user is 1261 added to certain groups. For those settings to take effect systemwide 1262 requires the user to log out and log back in. In the scenario where the 1263 user has run setup and hasn't logged out, we still want them to be able to 1264 launch a local instance so add the user to the groups as part of the 1265 command to ensure success. 1266 1267 The reason using here-doc instead of '&' is all operations need to be ran in 1268 ths same pid. Here's an example cmd: 1269 $ sg kvm << EOF 1270 sg libvirt 1271 sg cvdnetwork 1272 launch_cvd --cpus 2 --x_res 1280 --y_res 720 --dpi 160 --memory_mb 4096 1273 EOF 1274 1275 Args: 1276 cmd: String of the command to prepend the user groups to. 1277 user_groups: List of user groups name.(String) 1278 1279 Returns: 1280 String of the command with the user groups prepended to it if necessary, 1281 otherwise the same existing command. 1282 """ 1283 user_group_cmd = "" 1284 if not CheckUserInGroups(user_groups): 1285 logger.debug("Need to add user groups to the command") 1286 for idx, group in enumerate(user_groups): 1287 user_group_cmd += _CMD_SG + group 1288 if idx == 0: 1289 user_group_cmd += " <<EOF\n" 1290 else: 1291 user_group_cmd += "\n" 1292 cmd += "\nEOF" 1293 user_group_cmd += cmd 1294 logger.debug("user group cmd: %s", user_group_cmd) 1295 return user_group_cmd 1296 1297 1298def CheckUserInGroups(group_name_list): 1299 """Check if the current user is in the group. 1300 1301 Args: 1302 group_name_list: The list of group name. 1303 Returns: 1304 True if current user is in all the groups. 1305 """ 1306 logger.info("Checking if user is in following groups: %s", group_name_list) 1307 all_groups = [g.gr_name for g in grp.getgrall()] 1308 for group in group_name_list: 1309 if group not in all_groups: 1310 logger.info("This group doesn't exist: %s", group) 1311 return False 1312 if getpass.getuser() not in grp.getgrnam(group).gr_mem: 1313 logger.info("Current user isn't in this group: %s", group) 1314 return False 1315 return True 1316 1317 1318def IsSupportedPlatform(print_warning=False): 1319 """Check if user's os is the supported platform. 1320 1321 platform.version() return such as '#1 SMP Debian 5.6.14-1rodete2...' 1322 and use to judge supported or not. 1323 1324 Args: 1325 print_warning: Boolean, print the unsupported warning 1326 if True. 1327 Returns: 1328 Boolean, True if user is using supported platform. 1329 """ 1330 system = platform.system() 1331 # TODO(b/161085678): After python3 fully migrated, then use distro to fix. 1332 platform_supported = False 1333 if system in _SUPPORTED_SYSTEMS_AND_DISTS: 1334 for dist in _SUPPORTED_SYSTEMS_AND_DISTS[system]: 1335 if dist in platform.version(): 1336 platform_supported = True 1337 break 1338 1339 logger.info("Updated supported system and dists: %s", 1340 _SUPPORTED_SYSTEMS_AND_DISTS) 1341 platform_supported_msg = ("%s[%s] %s supported platform" % 1342 (system, 1343 platform.version(), 1344 "is a" if platform_supported else "is not a")) 1345 if print_warning and not platform_supported: 1346 PrintColorString(platform_supported_msg, TextColors.WARNING) 1347 else: 1348 logger.info(platform_supported_msg) 1349 1350 return platform_supported 1351 1352def IsSupportedKvm(): 1353 """Check if support kvm. 1354 1355 Returns: 1356 True if environment supported kvm. 1357 """ 1358 if os.path.exists(_KVM_PATH): 1359 return True 1360 1361 PrintColorString( 1362 "The environment doesn't support virtualization. Please run " 1363 "the remote instance by \"acloud create\" instead. If you want to " 1364 "launch AVD on the local instance, Please refer to http://go/" 1365 "acloud-cloudtop#acloud-create-local-instance-on-the-cloudtop", 1366 TextColors.FAIL) 1367 return False 1368 1369def GetDistDir(): 1370 """Return the absolute path to the dist dir.""" 1371 android_build_top = os.environ.get(constants.ENV_ANDROID_BUILD_TOP) 1372 if not android_build_top: 1373 return None 1374 dist_cmd = GET_BUILD_VAR_CMD[:] 1375 dist_cmd.append(_DIST_DIR) 1376 try: 1377 dist_dir = CheckOutput(dist_cmd, cwd=android_build_top) 1378 except subprocess.CalledProcessError: 1379 return None 1380 return os.path.join(android_build_top, dist_dir.strip()) 1381 1382 1383def CleanupProcess(pattern): 1384 """Cleanup process with pattern. 1385 1386 Args: 1387 pattern: String, string of process pattern. 1388 """ 1389 if IsCommandRunning(pattern): 1390 command_kill = _CMD_KILL + [pattern] 1391 subprocess.check_call(command_kill) 1392 1393 1394def TimeoutException(timeout_secs, timeout_error=_DEFAULT_TIMEOUT_ERR): 1395 """Decorater which function timeout setup and raise custom exception. 1396 1397 Args: 1398 timeout_secs: Number of maximum seconds of waiting time. 1399 timeout_error: String to describe timeout exception. 1400 1401 Returns: 1402 The function wrapper. 1403 """ 1404 if timeout_error == _DEFAULT_TIMEOUT_ERR: 1405 timeout_error = timeout_error % timeout_secs 1406 1407 def _Wrapper(func): 1408 # pylint: disable=unused-argument 1409 def _HandleTimeout(signum, frame): 1410 raise errors.FunctionTimeoutError(timeout_error) 1411 1412 def _FunctionWrapper(*args, **kwargs): 1413 signal.signal(signal.SIGALRM, _HandleTimeout) 1414 signal.alarm(timeout_secs) 1415 try: 1416 result = func(*args, **kwargs) 1417 finally: 1418 signal.alarm(0) 1419 return result 1420 1421 return _FunctionWrapper 1422 1423 return _Wrapper 1424 1425 1426def GetBuildEnvironmentVariable(variable_name): 1427 """Get build environment variable. 1428 1429 Args: 1430 variable_name: String of variable name. 1431 1432 Returns: 1433 String, the value of the variable. 1434 1435 Raises: 1436 errors.GetAndroidBuildEnvVarError: No environment variable found. 1437 """ 1438 try: 1439 return os.environ[variable_name] 1440 except KeyError as no_env_error: 1441 raise errors.GetAndroidBuildEnvVarError( 1442 "Could not get environment var: %s\n" 1443 "Try to run 'source build/envsetup.sh && lunch <target>'" 1444 % variable_name 1445 ) from no_env_error 1446 1447 1448# pylint: disable=no-member,import-outside-toplevel 1449def FindExecutable(filename): 1450 """A compatibility function to find execution file path. 1451 1452 Args: 1453 filename: String of execution filename. 1454 1455 Returns: 1456 String: execution file path. 1457 """ 1458 try: 1459 from distutils.spawn import find_executable 1460 return find_executable(filename) 1461 except ImportError: 1462 return shutil.which(filename) 1463 1464 1465def GetDictItems(namedtuple_object): 1466 """A compatibility function to access the OrdereDict object from the given namedtuple object. 1467 1468 Args: 1469 namedtuple_object: namedtuple object. 1470 1471 Returns: 1472 collections.namedtuple._asdict().items() when using python3. 1473 """ 1474 return namedtuple_object._asdict().items() 1475 1476 1477def CleanupSSVncviewer(vnc_port): 1478 """Cleanup the old disconnected ssvnc viewer. 1479 1480 Args: 1481 vnc_port: Integer, port number of vnc. 1482 """ 1483 ssvnc_viewer_pattern = _SSVNC_VIEWER_PATTERN % {"vnc_port":vnc_port} 1484 CleanupProcess(ssvnc_viewer_pattern) 1485 1486 1487def CheckOutput(cmd, **kwargs): 1488 """Call subprocess.check_output to get output. 1489 1490 The subprocess.check_output return type is "bytes" in python 3, we have 1491 to convert bytes as string with .decode() in advance. 1492 1493 Args: 1494 cmd: String of command. 1495 **kwargs: dictionary of keyword based args to pass to func. 1496 1497 Return: 1498 String to command output. 1499 """ 1500 return subprocess.check_output(cmd, **kwargs).decode() 1501 1502 1503def Popen(*command, **popen_args): 1504 """Execute subprocess.Popen command and log the output. 1505 1506 This method waits for the process to terminate. It kills the process 1507 if it's interrupted due to timeout. 1508 1509 Args: 1510 command: Strings, the command. 1511 popen_kwargs: The arguments to be passed to subprocess.Popen. 1512 1513 Raises: 1514 errors.SubprocessFail if the process returns non-zero. 1515 """ 1516 proc = None 1517 try: 1518 logger.info("Execute %s", command) 1519 popen_args["stdin"] = subprocess.PIPE 1520 popen_args["stdout"] = subprocess.PIPE 1521 popen_args["stderr"] = subprocess.PIPE 1522 1523 # Some OTA tools are Python scripts in different versions. The 1524 # PYTHONPATH for acloud may be incompatible with the tools. 1525 if "env" not in popen_args and "PYTHONPATH" in os.environ: 1526 popen_env = os.environ.copy() 1527 del popen_env["PYTHONPATH"] 1528 popen_args["env"] = popen_env 1529 1530 proc = subprocess.Popen(command, **popen_args) 1531 stdout, stderr = proc.communicate() 1532 logger.info("%s stdout: %s", command[0], stdout) 1533 logger.info("%s stderr: %s", command[0], stderr) 1534 1535 if proc.returncode != 0: 1536 raise errors.SubprocessFail("%s returned %d." % 1537 (command[0], proc.returncode)) 1538 finally: 1539 if proc and proc.poll() is None: 1540 logger.info("Kill %s", command[0]) 1541 proc.kill() 1542 1543 1544def SetExecutable(path): 1545 """Grant the persmission to execute a file. 1546 1547 Args: 1548 path: String, the file path. 1549 1550 Raises: 1551 OSError if any file operation fails. 1552 """ 1553 mode = os.stat(path).st_mode 1554 os.chmod(path, mode | (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | 1555 stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)) 1556 1557 1558def SetDirectoryTreeExecutable(dir_path): 1559 """Grant the permission to execute all files in a directory. 1560 1561 Args: 1562 dir_path: String, the directory path. 1563 1564 Raises: 1565 OSError if any file operation fails. 1566 """ 1567 for parent_dir, _, file_names in os.walk(dir_path): 1568 for name in file_names: 1569 SetExecutable(os.path.join(parent_dir, name)) 1570 1571 1572def GetCvdPorts(): 1573 """Get CVD ports 1574 1575 1576 Returns: 1577 ForwardedPorts: vnc port and adb port. 1578 """ 1579 return AVD_PORT_DICT[constants.TYPE_CF] 1580 1581 1582def SetCvdPorts(base_instance_num): 1583 """Adjust ports by base_instance_num. 1584 1585 Args: 1586 base_instance_num: int, cuttlefish base_instance_num. 1587 """ 1588 offset = (base_instance_num or 1) - 1 1589 AVD_PORT_DICT[constants.TYPE_CF] = ForwardedPorts( 1590 constants.CF_VNC_PORT + offset, constants.CF_ADB_PORT + offset) 1591 1592 # TODO: adjust WebRTC ports 1593