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