1#!/usr/bin/env python 2# 3# Copyright 2016 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16r""" 17Welcome to 18 ___ _______ ____ __ _____ 19 / _ |/ ___/ / / __ \/ / / / _ \ 20 / __ / /__/ /__/ /_/ / /_/ / // / 21/_/ |_\___/____/\____/\____/____/ 22 23 24This a tool to create Android Virtual Devices locally/remotely. 25 26- Prerequisites: 27 The manual will be available at 28 https://android.googlesource.com/platform/tools/acloud/+/master/README.md 29 30- To get started: 31 - Create instances: 32 1) To create a remote cuttlefish instance with the local built image. 33 Example: 34 $ acloud create --local-image 35 Or specify built image dir: 36 $ acloud create --local-image /tmp/image_dir 37 2) To create a local cuttlefish instance using the image which has been 38 built out in your workspace. 39 Example: 40 $ acloud create --local-instance --local-image 41 42 - Delete instances: 43 $ acloud delete 44 45 - Reconnect: 46 To reconnect adb/vnc to an existing instance that's been disconnected: 47 $ acloud reconnect 48 Or to specify a specific instance: 49 $ acloud reconnect --instance-names <instance_name like ins-123-cf-x86-phone> 50 51 - List: 52 List will retrieve all the remote instances you've created in addition to any 53 local instances created as well. 54 To show device IP address, adb port and instance name: 55 $ acloud list 56 To show more detail info on the list. 57 $ acloud list -vv 58 59- Pull: 60 Pull will download log files or show the log file in screen from one remote 61 cuttlefish instance: 62 $ acloud pull 63 Pull from a specified instance: 64 $ acloud pull --instance-name "your_instance_name" 65 66Try $acloud [cmd] --help for further details. 67 68""" 69 70from __future__ import print_function 71import argparse 72import logging 73import os 74import platform 75import sys 76import sysconfig 77import traceback 78 79# TODO: Remove this once we switch over to embedded launcher. 80# Exit out if python version is < 2.7.13 due to b/120883119. 81if (sys.version_info.major == 2 82 and sys.version_info.minor == 7 83 and sys.version_info.micro < 13): 84 print("Acloud requires python version 2.7.13+ (currently @ %d.%d.%d)" % 85 (sys.version_info.major, sys.version_info.minor, 86 sys.version_info.micro)) 87 print("Update your 2.7 python with:") 88 # pylint: disable=invalid-name 89 os_type = platform.system().lower() 90 if os_type == "linux": 91 print(" apt-get install python2.7") 92 elif os_type == "darwin": 93 print(" brew install python@2 (and then follow instructions at " 94 "https://docs.python-guide.org/starting/install/osx/)") 95 print(" - or -") 96 print(" POSIXLY_CORRECT=1 port -N install python27") 97 sys.exit(1) 98# This is a workaround to put '/usr/lib/python3.X' ahead of googleapiclient of 99# build system path list to fix python3 issue of http.client(b/144743252) 100# that googleapiclient existed http.py conflict with python3 build-in lib. 101# Using embedded_launcher(b/135639220) perhaps work whereas it didn't solve yet. 102if sys.version_info.major == 3: 103 sys.path.insert(0, os.path.dirname(sysconfig.get_paths()['purelib'])) 104 105# By Default silence root logger's stream handler since 3p lib may initial 106# root logger no matter what level we're using. The acloud logger behavior will 107# be defined in _SetupLogging(). This also could workaround to get rid of below 108# oauth2client warning: 109# 'No handlers could be found for logger "oauth2client.contrib.multistore_file' 110DEFAULT_STREAM_HANDLER = logging.StreamHandler() 111DEFAULT_STREAM_HANDLER.setLevel(logging.CRITICAL) 112logging.getLogger().addHandler(DEFAULT_STREAM_HANDLER) 113 114# pylint: disable=wrong-import-position 115from acloud import errors 116from acloud.create import create 117from acloud.create import create_args 118from acloud.delete import delete 119from acloud.delete import delete_args 120from acloud.internal import constants 121from acloud.reconnect import reconnect 122from acloud.reconnect import reconnect_args 123from acloud.list import list as list_instances 124from acloud.list import list_args 125from acloud.metrics import metrics 126from acloud.powerwash import powerwash 127from acloud.powerwash import powerwash_args 128from acloud.public import acloud_common 129from acloud.public import config 130from acloud.public import report 131from acloud.public.actions import create_cuttlefish_action 132from acloud.public.actions import create_goldfish_action 133from acloud.pull import pull 134from acloud.pull import pull_args 135from acloud.restart import restart 136from acloud.restart import restart_args 137from acloud.setup import setup 138from acloud.setup import setup_args 139 140 141LOGGING_FMT = "%(asctime)s |%(levelname)s| %(module)s:%(lineno)s| %(message)s" 142ACLOUD_LOGGER = "acloud" 143_LOGGER = logging.getLogger(ACLOUD_LOGGER) 144NO_ERROR_MESSAGE = "" 145PROG = "acloud" 146_ACLOUD_CONFIG_ERROR = "ACLOUD_CONFIG_ERROR" 147 148# Commands 149CMD_CREATE_CUTTLEFISH = "create_cf" 150CMD_CREATE_GOLDFISH = "create_gf" 151 152# Config requires fields. 153_CREATE_REQUIRE_FIELDS = ["project", "zone", "machine_type"] 154_CREATE_CF_REQUIRE_FIELDS = ["resolution"] 155# show contact info to user. 156_CONTACT_INFO = ("If you have any question or need acloud team support, " 157 "please feel free to contact us by email at " 158 "buganizer-system+419709@google.com") 159_LOG_INFO = " and attach those log files from %s" 160 161 162# pylint: disable=too-many-statements 163def _ParseArgs(args): 164 """Parse args. 165 166 Args: 167 args: Argument list passed from main. 168 169 Returns: 170 Parsed args. 171 """ 172 usage = ",".join([ 173 setup_args.CMD_SETUP, 174 create_args.CMD_CREATE, 175 list_args.CMD_LIST, 176 delete_args.CMD_DELETE, 177 reconnect_args.CMD_RECONNECT, 178 pull_args.CMD_PULL, 179 restart_args.CMD_RESTART, 180 ]) 181 parser = argparse.ArgumentParser( 182 description=__doc__, 183 formatter_class=argparse.RawDescriptionHelpFormatter, 184 usage="acloud {" + usage + "} ...") 185 parser = argparse.ArgumentParser(prog=PROG) 186 parser.add_argument('--version', action='version', version=( 187 '%(prog)s ' + config.GetVersion())) 188 subparsers = parser.add_subparsers(metavar="{" + usage + "}") 189 subparser_list = [] 190 191 # Command "create_cf", create cuttlefish instances 192 create_cf_parser = subparsers.add_parser(CMD_CREATE_CUTTLEFISH) 193 create_cf_parser.required = False 194 create_cf_parser.set_defaults(which=CMD_CREATE_CUTTLEFISH) 195 create_args.AddCommonCreateArgs(create_cf_parser) 196 subparser_list.append(create_cf_parser) 197 198 # Command "create_gf", create goldfish instances 199 # In order to create a goldfish device we need the following parameters: 200 # 1. The emulator build we wish to use, this is the binary that emulates 201 # an android device. See go/emu-dev for more 202 # 2. A system-image. This is the android release we wish to run on the 203 # emulated hardware. 204 create_gf_parser = subparsers.add_parser(CMD_CREATE_GOLDFISH) 205 create_gf_parser.required = False 206 create_gf_parser.set_defaults(which=CMD_CREATE_GOLDFISH) 207 create_gf_parser.add_argument( 208 "--emulator_build_id", 209 type=str, 210 dest="emulator_build_id", 211 required=False, 212 help="Emulator build used to run the images. e.g. 4669466.") 213 create_gf_parser.add_argument( 214 "--emulator_branch", 215 type=str, 216 dest="emulator_branch", 217 required=False, 218 help="Emulator build branch name, e.g. aosp-emu-master-dev. If specified" 219 " without emulator_build_id, the last green build will be used.") 220 create_gf_parser.add_argument( 221 "--base_image", 222 type=str, 223 dest="base_image", 224 required=False, 225 help="Name of the goldfish base image to be used to create the instance. " 226 "This will override stable_goldfish_host_image_name from config. " 227 "e.g. emu-dev-cts-061118") 228 create_gf_parser.add_argument( 229 "--tags", 230 dest="tags", 231 nargs="*", 232 required=False, 233 default=None, 234 help="Tags to be set on to the created instance. e.g. https-server.") 235 236 create_args.AddCommonCreateArgs(create_gf_parser) 237 subparser_list.append(create_gf_parser) 238 239 # Command "create" 240 subparser_list.append(create_args.GetCreateArgParser(subparsers)) 241 242 # Command "setup" 243 subparser_list.append(setup_args.GetSetupArgParser(subparsers)) 244 245 # Command "delete" 246 subparser_list.append(delete_args.GetDeleteArgParser(subparsers)) 247 248 # Command "list" 249 subparser_list.append(list_args.GetListArgParser(subparsers)) 250 251 # Command "reconnect" 252 subparser_list.append(reconnect_args.GetReconnectArgParser(subparsers)) 253 254 # Command "restart" 255 subparser_list.append(restart_args.GetRestartArgParser(subparsers)) 256 257 # Command "powerwash" 258 subparser_list.append(powerwash_args.GetPowerwashArgParser(subparsers)) 259 260 # Command "pull" 261 subparser_list.append(pull_args.GetPullArgParser(subparsers)) 262 263 # Add common arguments. 264 for subparser in subparser_list: 265 acloud_common.AddCommonArguments(subparser) 266 267 if not args: 268 parser.print_help() 269 sys.exit(constants.EXIT_BY_WRONG_CMD) 270 271 return parser.parse_args(args) 272 273 274# pylint: disable=too-many-branches 275def _VerifyArgs(parsed_args): 276 """Verify args. 277 278 Args: 279 parsed_args: Parsed args. 280 281 Raises: 282 errors.CommandArgError: If args are invalid. 283 errors.UnsupportedCreateArgs: When a create arg is specified but 284 unsupported for a particular avd type. 285 (e.g. --system-build-id for gf) 286 """ 287 if parsed_args.which == create_args.CMD_CREATE: 288 create_args.VerifyArgs(parsed_args) 289 if parsed_args.which == setup_args.CMD_SETUP: 290 setup_args.VerifyArgs(parsed_args) 291 if parsed_args.which == CMD_CREATE_CUTTLEFISH: 292 if not parsed_args.build_id and not parsed_args.branch: 293 raise errors.CommandArgError( 294 "Must specify --build_id or --branch") 295 if parsed_args.which == CMD_CREATE_GOLDFISH: 296 if not parsed_args.emulator_build_id and not parsed_args.build_id and ( 297 not parsed_args.emulator_branch and not parsed_args.branch): 298 raise errors.CommandArgError( 299 "Must specify either --build_id or --branch or " 300 "--emulator_branch or --emulator_build_id") 301 if not parsed_args.build_target: 302 raise errors.CommandArgError("Must specify --build_target") 303 if (parsed_args.system_branch 304 or parsed_args.system_build_id 305 or parsed_args.system_build_target): 306 raise errors.UnsupportedCreateArgs( 307 "--system-* args are not supported for AVD type: %s" 308 % constants.TYPE_GF) 309 310 if parsed_args.which in [ 311 create_args.CMD_CREATE, CMD_CREATE_CUTTLEFISH, CMD_CREATE_GOLDFISH 312 ]: 313 if (parsed_args.serial_log_file 314 and not parsed_args.serial_log_file.endswith(".tar.gz")): 315 raise errors.CommandArgError( 316 "--serial_log_file must ends with .tar.gz") 317 318 319def _ParsingConfig(args, cfg): 320 """Parse config to check if missing any field. 321 322 Args: 323 args: Namespace object from argparse.parse_args. 324 cfg: AcloudConfig object. 325 326 Returns: 327 error message about list of missing config fields. 328 """ 329 missing_fields = [] 330 if args.which == create_args.CMD_CREATE and args.local_instance is None: 331 missing_fields = cfg.GetMissingFields(_CREATE_REQUIRE_FIELDS) 332 if args.which == CMD_CREATE_CUTTLEFISH: 333 missing_fields.extend(cfg.GetMissingFields(_CREATE_CF_REQUIRE_FIELDS)) 334 if missing_fields: 335 return "Missing required configuration fields: %s" % missing_fields 336 return None 337 338 339def _SetupLogging(log_file, verbose): 340 """Setup logging. 341 342 This function define the logging policy in below manners. 343 - without -v , -vv ,--log_file: 344 Only display critical log and print() message on screen. 345 346 - with -v: 347 Display INFO log and set StreamHandler to acloud parent logger to turn on 348 ONLY acloud modules logging.(silence all 3p libraries) 349 350 - with -vv: 351 Display INFO/DEBUG log and set StreamHandler to root logger to turn on all 352 acloud modules and 3p libraries logging. 353 354 - with --log_file. 355 Dump logs to FileHandler with DEBUG level. 356 357 Args: 358 log_file: String, if not None, dump the log to log file. 359 verbose: Int, if verbose = 1(-v), log at INFO level and turn on 360 logging on libraries to a StreamHandler. 361 If verbose = 2(-vv), log at DEBUG level and turn on logging on 362 all libraries and 3rd party libraries to a StreamHandler. 363 """ 364 # Define logging level and hierarchy by verbosity. 365 shandler_level = None 366 logger = None 367 if verbose == 0: 368 shandler_level = logging.CRITICAL 369 logger = logging.getLogger(ACLOUD_LOGGER) 370 elif verbose == 1: 371 shandler_level = logging.INFO 372 logger = logging.getLogger(ACLOUD_LOGGER) 373 elif verbose > 1: 374 shandler_level = logging.DEBUG 375 logger = logging.getLogger() 376 377 # Add StreamHandler by default. 378 shandler = logging.StreamHandler() 379 shandler.setFormatter(logging.Formatter(LOGGING_FMT)) 380 shandler.setLevel(shandler_level) 381 logger.addHandler(shandler) 382 # Set the default level to DEBUG, the other handlers will handle 383 # their own levels via the args supplied (-v and --log_file). 384 logger.setLevel(logging.DEBUG) 385 386 # Add FileHandler if log_file is provided. 387 if log_file: 388 fhandler = logging.FileHandler(filename=log_file) 389 fhandler.setFormatter(logging.Formatter(LOGGING_FMT)) 390 fhandler.setLevel(logging.DEBUG) 391 logger.addHandler(fhandler) 392 393 394def main(argv=None): 395 """Main entry. 396 397 Args: 398 argv: A list of system arguments. 399 400 Returns: 401 Job status: Integer, 0 if success. None-zero if fails. 402 Stack trace: String of errors. 403 """ 404 args = _ParseArgs(argv) 405 _SetupLogging(args.log_file, args.verbose) 406 _VerifyArgs(args) 407 _LOGGER.info("Acloud version: %s", config.GetVersion()) 408 409 cfg = config.GetAcloudConfig(args) 410 parsing_config_error = _ParsingConfig(args, cfg) 411 # TODO: Move this check into the functions it is actually needed. 412 # Check access. 413 # device_driver.CheckAccess(cfg) 414 415 reporter = None 416 if parsing_config_error: 417 reporter = report.Report(command=args.which) 418 reporter.UpdateFailure(parsing_config_error, _ACLOUD_CONFIG_ERROR) 419 elif args.which == create_args.CMD_CREATE: 420 reporter = create.Run(args) 421 elif args.which == CMD_CREATE_CUTTLEFISH: 422 reporter = create_cuttlefish_action.CreateDevices( 423 cfg=cfg, 424 build_target=args.build_target, 425 build_id=args.build_id, 426 branch=args.branch, 427 kernel_build_id=args.kernel_build_id, 428 kernel_branch=args.kernel_branch, 429 kernel_build_target=args.kernel_build_target, 430 system_branch=args.system_branch, 431 system_build_id=args.system_build_id, 432 system_build_target=args.system_build_target, 433 bootloader_branch=args.bootloader_branch, 434 bootloader_build_id=args.bootloader_build_id, 435 bootloader_build_target=args.bootloader_build_target, 436 gpu=args.gpu, 437 num=args.num, 438 serial_log_file=args.serial_log_file, 439 autoconnect=args.autoconnect, 440 report_internal_ip=args.report_internal_ip, 441 boot_timeout_secs=args.boot_timeout_secs, 442 ins_timeout_secs=args.ins_timeout_secs) 443 elif args.which == CMD_CREATE_GOLDFISH: 444 reporter = create_goldfish_action.CreateDevices( 445 cfg=cfg, 446 build_target=args.build_target, 447 build_id=args.build_id, 448 emulator_build_id=args.emulator_build_id, 449 branch=args.branch, 450 emulator_branch=args.emulator_branch, 451 kernel_build_id=args.kernel_build_id, 452 kernel_branch=args.kernel_branch, 453 kernel_build_target=args.kernel_build_target, 454 gpu=args.gpu, 455 num=args.num, 456 serial_log_file=args.serial_log_file, 457 autoconnect=args.autoconnect, 458 tags=args.tags, 459 report_internal_ip=args.report_internal_ip, 460 boot_timeout_secs=args.boot_timeout_secs) 461 elif args.which == delete_args.CMD_DELETE: 462 reporter = delete.Run(args) 463 elif args.which == list_args.CMD_LIST: 464 list_instances.Run(args) 465 elif args.which == reconnect_args.CMD_RECONNECT: 466 reconnect.Run(args) 467 elif args.which == restart_args.CMD_RESTART: 468 reporter = restart.Run(args) 469 elif args.which == powerwash_args.CMD_POWERWASH: 470 reporter = powerwash.Run(args) 471 elif args.which == pull_args.CMD_PULL: 472 reporter = pull.Run(args) 473 elif args.which == setup_args.CMD_SETUP: 474 setup.Run(args) 475 else: 476 error_msg = "Invalid command %s" % args.which 477 sys.stderr.write(error_msg) 478 return constants.EXIT_BY_WRONG_CMD, error_msg 479 480 if reporter and args.report_file: 481 reporter.Dump(args.report_file) 482 if reporter and reporter.errors: 483 error_msg = "\n".join(reporter.errors) 484 help_msg = _CONTACT_INFO 485 if reporter.data.get(constants.ERROR_LOG_FOLDER): 486 help_msg += _LOG_INFO % reporter.data.get(constants.ERROR_LOG_FOLDER) 487 sys.stderr.write("Encountered the following errors:\n%s\n\n%s.\n" % 488 (error_msg, help_msg)) 489 return constants.EXIT_BY_FAIL_REPORT, error_msg 490 return constants.EXIT_SUCCESS, NO_ERROR_MESSAGE 491 492 493if __name__ == "__main__": 494 EXIT_CODE = None 495 EXCEPTION_STACKTRACE = None 496 EXCEPTION_LOG = None 497 LOG_METRICS = metrics.LogUsage(sys.argv[1:]) 498 try: 499 EXIT_CODE, EXCEPTION_STACKTRACE = main(sys.argv[1:]) 500 except Exception as e: 501 EXIT_CODE = constants.EXIT_BY_ERROR 502 EXCEPTION_STACKTRACE = traceback.format_exc() 503 EXCEPTION_LOG = str(e) 504 raise 505 finally: 506 # Log Exit event here to calculate the consuming time. 507 if LOG_METRICS: 508 metrics.LogExitEvent(EXIT_CODE, 509 stacktrace=EXCEPTION_STACKTRACE, 510 logs=EXCEPTION_LOG) 511