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