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