#!/usr/bin/env python # # Copyright 2016 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. r""" Welcome to ___ _______ ____ __ _____ / _ |/ ___/ / / __ \/ / / / _ \ / __ / /__/ /__/ /_/ / /_/ / // / /_/ |_\___/____/\____/\____/____/ This a tool to create Android Virtual Devices locally/remotely. - Prerequisites: The manual will be available at https://android.googlesource.com/platform/tools/acloud/+/master/README.md - To get started: - Create instances: 1) To create a remote cuttlefish instance with the local built image. Example: $ acloud create --local-image Or specify built image dir: $ acloud create --local-image /tmp/image_dir 2) To create a local cuttlefish instance using the image which has been built out in your workspace. Example: $ acloud create --local-instance --local-image - Delete instances: $ acloud delete - Reconnect: To reconnect adb/vnc to an existing instance that's been disconnected: $ acloud reconnect Or to specify a specific instance: $ acloud reconnect --instance-names - List: List will retrieve all the remote instances you've created in addition to any local instances created as well. To show device IP address, adb port and instance name: $ acloud list To show more detail info on the list. $ acloud list -vv - Pull: Pull will download log files or show the log file in screen from one remote cuttlefish instance: $ acloud pull Pull from a specified instance: $ acloud pull --instance-name "your_instance_name" Try $acloud [cmd] --help for further details. """ from __future__ import print_function import argparse import logging import os import platform import sys import sysconfig import traceback # TODO: Remove this once we switch over to embedded launcher. # Exit out if python version is < 2.7.13 due to b/120883119. if (sys.version_info.major == 2 and sys.version_info.minor == 7 and sys.version_info.micro < 13): print("Acloud requires python version 2.7.13+ (currently @ %d.%d.%d)" % (sys.version_info.major, sys.version_info.minor, sys.version_info.micro)) print("Update your 2.7 python with:") # pylint: disable=invalid-name os_type = platform.system().lower() if os_type == "linux": print(" apt-get install python2.7") elif os_type == "darwin": print(" brew install python@2 (and then follow instructions at " "https://docs.python-guide.org/starting/install/osx/)") print(" - or -") print(" POSIXLY_CORRECT=1 port -N install python27") sys.exit(1) # This is a workaround to put '/usr/lib/python3.X' ahead of googleapiclient of # build system path list to fix python3 issue of http.client(b/144743252) # that googleapiclient existed http.py conflict with python3 build-in lib. # Using embedded_launcher(b/135639220) perhaps work whereas it didn't solve yet. if sys.version_info.major == 3: sys.path.insert(0, os.path.dirname(sysconfig.get_paths()['purelib'])) # By Default silence root logger's stream handler since 3p lib may initial # root logger no matter what level we're using. The acloud logger behavior will # be defined in _SetupLogging(). This also could workaround to get rid of below # oauth2client warning: # 'No handlers could be found for logger "oauth2client.contrib.multistore_file' DEFAULT_STREAM_HANDLER = logging.StreamHandler() DEFAULT_STREAM_HANDLER.setLevel(logging.CRITICAL) logging.getLogger().addHandler(DEFAULT_STREAM_HANDLER) # pylint: disable=wrong-import-position from acloud import errors from acloud.create import create from acloud.create import create_args from acloud.delete import delete from acloud.delete import delete_args from acloud.internal import constants from acloud.reconnect import reconnect from acloud.reconnect import reconnect_args from acloud.list import list as list_instances from acloud.list import list_args from acloud.metrics import metrics from acloud.powerwash import powerwash from acloud.powerwash import powerwash_args from acloud.public import acloud_common from acloud.public import config from acloud.public import report from acloud.public.actions import create_cuttlefish_action from acloud.public.actions import create_goldfish_action from acloud.pull import pull from acloud.pull import pull_args from acloud.restart import restart from acloud.restart import restart_args from acloud.setup import setup from acloud.setup import setup_args LOGGING_FMT = "%(asctime)s |%(levelname)s| %(module)s:%(lineno)s| %(message)s" ACLOUD_LOGGER = "acloud" _LOGGER = logging.getLogger(ACLOUD_LOGGER) NO_ERROR_MESSAGE = "" PROG = "acloud" _ACLOUD_CONFIG_ERROR = "ACLOUD_CONFIG_ERROR" # Commands CMD_CREATE_CUTTLEFISH = "create_cf" CMD_CREATE_GOLDFISH = "create_gf" # Config requires fields. _CREATE_REQUIRE_FIELDS = ["project", "zone", "machine_type"] _CREATE_CF_REQUIRE_FIELDS = ["resolution"] # show contact info to user. _CONTACT_INFO = ("If you have any question or need acloud team support, " "please feel free to contact us by email at " "buganizer-system+419709@google.com") _LOG_INFO = " and attach those log files from %s" # pylint: disable=too-many-statements def _ParseArgs(args): """Parse args. Args: args: Argument list passed from main. Returns: Parsed args. """ usage = ",".join([ setup_args.CMD_SETUP, create_args.CMD_CREATE, list_args.CMD_LIST, delete_args.CMD_DELETE, reconnect_args.CMD_RECONNECT, pull_args.CMD_PULL, restart_args.CMD_RESTART, ]) parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, usage="acloud {" + usage + "} ...") parser = argparse.ArgumentParser(prog=PROG) parser.add_argument('--version', action='version', version=( '%(prog)s ' + config.GetVersion())) subparsers = parser.add_subparsers(metavar="{" + usage + "}") subparser_list = [] # Command "create_cf", create cuttlefish instances create_cf_parser = subparsers.add_parser(CMD_CREATE_CUTTLEFISH) create_cf_parser.required = False create_cf_parser.set_defaults(which=CMD_CREATE_CUTTLEFISH) create_args.AddCommonCreateArgs(create_cf_parser) subparser_list.append(create_cf_parser) # Command "create_gf", create goldfish instances # In order to create a goldfish device we need the following parameters: # 1. The emulator build we wish to use, this is the binary that emulates # an android device. See go/emu-dev for more # 2. A system-image. This is the android release we wish to run on the # emulated hardware. create_gf_parser = subparsers.add_parser(CMD_CREATE_GOLDFISH) create_gf_parser.required = False create_gf_parser.set_defaults(which=CMD_CREATE_GOLDFISH) create_gf_parser.add_argument( "--emulator_build_id", type=str, dest="emulator_build_id", required=False, help="Emulator build used to run the images. e.g. 4669466.") create_gf_parser.add_argument( "--emulator_branch", type=str, dest="emulator_branch", required=False, help="Emulator build branch name, e.g. aosp-emu-master-dev. If specified" " without emulator_build_id, the last green build will be used.") create_gf_parser.add_argument( "--base_image", type=str, dest="base_image", required=False, help="Name of the goldfish base image to be used to create the instance. " "This will override stable_goldfish_host_image_name from config. " "e.g. emu-dev-cts-061118") create_gf_parser.add_argument( "--tags", dest="tags", nargs="*", required=False, default=None, help="Tags to be set on to the created instance. e.g. https-server.") create_args.AddCommonCreateArgs(create_gf_parser) subparser_list.append(create_gf_parser) # Command "create" subparser_list.append(create_args.GetCreateArgParser(subparsers)) # Command "setup" subparser_list.append(setup_args.GetSetupArgParser(subparsers)) # Command "delete" subparser_list.append(delete_args.GetDeleteArgParser(subparsers)) # Command "list" subparser_list.append(list_args.GetListArgParser(subparsers)) # Command "reconnect" subparser_list.append(reconnect_args.GetReconnectArgParser(subparsers)) # Command "restart" subparser_list.append(restart_args.GetRestartArgParser(subparsers)) # Command "powerwash" subparser_list.append(powerwash_args.GetPowerwashArgParser(subparsers)) # Command "pull" subparser_list.append(pull_args.GetPullArgParser(subparsers)) # Add common arguments. for subparser in subparser_list: acloud_common.AddCommonArguments(subparser) if not args: parser.print_help() sys.exit(constants.EXIT_BY_WRONG_CMD) return parser.parse_args(args) # pylint: disable=too-many-branches def _VerifyArgs(parsed_args): """Verify args. Args: parsed_args: Parsed args. Raises: errors.CommandArgError: If args are invalid. errors.UnsupportedCreateArgs: When a create arg is specified but unsupported for a particular avd type. (e.g. --system-build-id for gf) """ if parsed_args.which == create_args.CMD_CREATE: create_args.VerifyArgs(parsed_args) if parsed_args.which == setup_args.CMD_SETUP: setup_args.VerifyArgs(parsed_args) if parsed_args.which == CMD_CREATE_CUTTLEFISH: if not parsed_args.build_id and not parsed_args.branch: raise errors.CommandArgError( "Must specify --build_id or --branch") if parsed_args.which == CMD_CREATE_GOLDFISH: if not parsed_args.emulator_build_id and not parsed_args.build_id and ( not parsed_args.emulator_branch and not parsed_args.branch): raise errors.CommandArgError( "Must specify either --build_id or --branch or " "--emulator_branch or --emulator_build_id") if not parsed_args.build_target: raise errors.CommandArgError("Must specify --build_target") if (parsed_args.system_branch or parsed_args.system_build_id or parsed_args.system_build_target): raise errors.UnsupportedCreateArgs( "--system-* args are not supported for AVD type: %s" % constants.TYPE_GF) if parsed_args.which in [ create_args.CMD_CREATE, CMD_CREATE_CUTTLEFISH, CMD_CREATE_GOLDFISH ]: if (parsed_args.serial_log_file and not parsed_args.serial_log_file.endswith(".tar.gz")): raise errors.CommandArgError( "--serial_log_file must ends with .tar.gz") def _ParsingConfig(args, cfg): """Parse config to check if missing any field. Args: args: Namespace object from argparse.parse_args. cfg: AcloudConfig object. Returns: error message about list of missing config fields. """ missing_fields = [] if args.which == create_args.CMD_CREATE and args.local_instance is None: missing_fields = cfg.GetMissingFields(_CREATE_REQUIRE_FIELDS) if args.which == CMD_CREATE_CUTTLEFISH: missing_fields.extend(cfg.GetMissingFields(_CREATE_CF_REQUIRE_FIELDS)) if missing_fields: return "Missing required configuration fields: %s" % missing_fields return None def _SetupLogging(log_file, verbose): """Setup logging. This function define the logging policy in below manners. - without -v , -vv ,--log_file: Only display critical log and print() message on screen. - with -v: Display INFO log and set StreamHandler to acloud parent logger to turn on ONLY acloud modules logging.(silence all 3p libraries) - with -vv: Display INFO/DEBUG log and set StreamHandler to root logger to turn on all acloud modules and 3p libraries logging. - with --log_file. Dump logs to FileHandler with DEBUG level. Args: log_file: String, if not None, dump the log to log file. verbose: Int, if verbose = 1(-v), log at INFO level and turn on logging on libraries to a StreamHandler. If verbose = 2(-vv), log at DEBUG level and turn on logging on all libraries and 3rd party libraries to a StreamHandler. """ # Define logging level and hierarchy by verbosity. shandler_level = None logger = None if verbose == 0: shandler_level = logging.CRITICAL logger = logging.getLogger(ACLOUD_LOGGER) elif verbose == 1: shandler_level = logging.INFO logger = logging.getLogger(ACLOUD_LOGGER) elif verbose > 1: shandler_level = logging.DEBUG logger = logging.getLogger() # Add StreamHandler by default. shandler = logging.StreamHandler() shandler.setFormatter(logging.Formatter(LOGGING_FMT)) shandler.setLevel(shandler_level) logger.addHandler(shandler) # Set the default level to DEBUG, the other handlers will handle # their own levels via the args supplied (-v and --log_file). logger.setLevel(logging.DEBUG) # Add FileHandler if log_file is provided. if log_file: fhandler = logging.FileHandler(filename=log_file) fhandler.setFormatter(logging.Formatter(LOGGING_FMT)) fhandler.setLevel(logging.DEBUG) logger.addHandler(fhandler) def main(argv=None): """Main entry. Args: argv: A list of system arguments. Returns: Job status: Integer, 0 if success. None-zero if fails. Stack trace: String of errors. """ args = _ParseArgs(argv) _SetupLogging(args.log_file, args.verbose) _VerifyArgs(args) _LOGGER.info("Acloud version: %s", config.GetVersion()) cfg = config.GetAcloudConfig(args) parsing_config_error = _ParsingConfig(args, cfg) # TODO: Move this check into the functions it is actually needed. # Check access. # device_driver.CheckAccess(cfg) reporter = None if parsing_config_error: reporter = report.Report(command=args.which) reporter.UpdateFailure(parsing_config_error, _ACLOUD_CONFIG_ERROR) elif args.which == create_args.CMD_CREATE: reporter = create.Run(args) elif args.which == CMD_CREATE_CUTTLEFISH: reporter = create_cuttlefish_action.CreateDevices( cfg=cfg, build_target=args.build_target, build_id=args.build_id, branch=args.branch, kernel_build_id=args.kernel_build_id, kernel_branch=args.kernel_branch, kernel_build_target=args.kernel_build_target, system_branch=args.system_branch, system_build_id=args.system_build_id, system_build_target=args.system_build_target, bootloader_branch=args.bootloader_branch, bootloader_build_id=args.bootloader_build_id, bootloader_build_target=args.bootloader_build_target, gpu=args.gpu, num=args.num, serial_log_file=args.serial_log_file, autoconnect=args.autoconnect, report_internal_ip=args.report_internal_ip, boot_timeout_secs=args.boot_timeout_secs, ins_timeout_secs=args.ins_timeout_secs) elif args.which == CMD_CREATE_GOLDFISH: reporter = create_goldfish_action.CreateDevices( cfg=cfg, build_target=args.build_target, build_id=args.build_id, emulator_build_id=args.emulator_build_id, branch=args.branch, emulator_branch=args.emulator_branch, kernel_build_id=args.kernel_build_id, kernel_branch=args.kernel_branch, kernel_build_target=args.kernel_build_target, gpu=args.gpu, num=args.num, serial_log_file=args.serial_log_file, autoconnect=args.autoconnect, tags=args.tags, report_internal_ip=args.report_internal_ip, boot_timeout_secs=args.boot_timeout_secs) elif args.which == delete_args.CMD_DELETE: reporter = delete.Run(args) elif args.which == list_args.CMD_LIST: list_instances.Run(args) elif args.which == reconnect_args.CMD_RECONNECT: reconnect.Run(args) elif args.which == restart_args.CMD_RESTART: reporter = restart.Run(args) elif args.which == powerwash_args.CMD_POWERWASH: reporter = powerwash.Run(args) elif args.which == pull_args.CMD_PULL: reporter = pull.Run(args) elif args.which == setup_args.CMD_SETUP: setup.Run(args) else: error_msg = "Invalid command %s" % args.which sys.stderr.write(error_msg) return constants.EXIT_BY_WRONG_CMD, error_msg if reporter and args.report_file: reporter.Dump(args.report_file) if reporter and reporter.errors: error_msg = "\n".join(reporter.errors) help_msg = _CONTACT_INFO if reporter.data.get(constants.ERROR_LOG_FOLDER): help_msg += _LOG_INFO % reporter.data.get(constants.ERROR_LOG_FOLDER) sys.stderr.write("Encountered the following errors:\n%s\n\n%s.\n" % (error_msg, help_msg)) return constants.EXIT_BY_FAIL_REPORT, error_msg return constants.EXIT_SUCCESS, NO_ERROR_MESSAGE if __name__ == "__main__": EXIT_CODE = None EXCEPTION_STACKTRACE = None EXCEPTION_LOG = None LOG_METRICS = metrics.LogUsage(sys.argv[1:]) try: EXIT_CODE, EXCEPTION_STACKTRACE = main(sys.argv[1:]) except Exception as e: EXIT_CODE = constants.EXIT_BY_ERROR EXCEPTION_STACKTRACE = traceback.format_exc() EXCEPTION_LOG = str(e) raise finally: # Log Exit event here to calculate the consuming time. if LOG_METRICS: metrics.LogExitEvent(EXIT_CODE, stacktrace=EXCEPTION_STACKTRACE, logs=EXCEPTION_LOG)