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 sys 75import traceback 76 77if sys.version_info.major == 2: 78 print("Acloud only supports python3 (currently @ %d.%d.%d)." 79 " Please run Acloud with python3." % (sys.version_info.major, 80 sys.version_info.minor, 81 sys.version_info.micro)) 82 sys.exit(1) 83 84# By Default silence root logger's stream handler since 3p lib may initial 85# root logger no matter what level we're using. The acloud logger behavior will 86# be defined in _SetupLogging(). This also could workaround to get rid of below 87# oauth2client warning: 88# 'No handlers could be found for logger "oauth2client.contrib.multistore_file' 89DEFAULT_STREAM_HANDLER = logging.StreamHandler() 90DEFAULT_STREAM_HANDLER.setLevel(logging.CRITICAL) 91logging.getLogger().addHandler(DEFAULT_STREAM_HANDLER) 92 93# pylint: disable=wrong-import-position 94from acloud import errors 95from acloud.create import create 96from acloud.create import create_args 97from acloud.delete import delete 98from acloud.delete import delete_args 99from acloud.internal import constants 100from acloud.reconnect import reconnect 101from acloud.reconnect import reconnect_args 102from acloud.list import list as list_instances 103from acloud.list import list_args 104from acloud.metrics import metrics 105from acloud.powerwash import powerwash 106from acloud.powerwash import powerwash_args 107from acloud.public import acloud_common 108from acloud.public import config 109from acloud.public import report 110from acloud.public.actions import create_goldfish_action 111from acloud.pull import pull 112from acloud.pull import pull_args 113from acloud.restart import restart 114from acloud.restart import restart_args 115from acloud.setup import setup 116from acloud.setup import setup_args 117from acloud.hostcleanup import hostcleanup 118from acloud.hostcleanup import hostcleanup_args 119 120 121LOGGING_FMT = "%(asctime)s |%(levelname)s| %(module)s:%(lineno)s| %(message)s" 122ACLOUD_LOGGER = "acloud" 123_LOGGER = logging.getLogger(ACLOUD_LOGGER) 124NO_ERROR_MESSAGE = "" 125PROG = "acloud" 126DEFAULT_SUPPORT_ARGS = ["--version", "-h", "--help"] 127 128# Commands 129CMD_CREATE_GOLDFISH = "create_gf" 130 131# Config requires fields. 132_CREATE_REQUIRE_FIELDS = ["project", "zone", "machine_type"] 133# show contact info to user. 134_CONTACT_INFO = ("If you have any question or need acloud team support, " 135 "please feel free to contact us by email at " 136 "[email protected]") 137_LOG_INFO = " and attach those log files from %s" 138 139 140# pylint: disable=too-many-statements 141def _ParseArgs(args): 142 """Parse args. 143 144 Args: 145 args: Argument list passed from main. 146 147 Returns: 148 Parsed args and a list of unknown argument strings. 149 """ 150 acloud_cmds = [ 151 setup_args.CMD_SETUP, 152 create_args.CMD_CREATE, 153 list_args.CMD_LIST, 154 delete_args.CMD_DELETE, 155 reconnect_args.CMD_RECONNECT, 156 powerwash_args.CMD_POWERWASH, 157 pull_args.CMD_PULL, 158 restart_args.CMD_RESTART, 159 hostcleanup_args.CMD_HOSTCLEANUP, 160 CMD_CREATE_GOLDFISH] 161 usage = ",".join(acloud_cmds) 162 parser = argparse.ArgumentParser( 163 description=__doc__, 164 formatter_class=argparse.RawDescriptionHelpFormatter, 165 usage="acloud {" + usage + "} ...") 166 parser = argparse.ArgumentParser(prog=PROG) 167 parser.add_argument('--version', action='version', version=( 168 '%(prog)s ' + config.GetVersion())) 169 subparsers = parser.add_subparsers(metavar="{" + usage + "}") 170 subparser_list = [] 171 172 # Command "create_gf", create goldfish instances 173 # In order to create a goldfish device we need the following parameters: 174 # 1. The emulator build we wish to use, this is the binary that emulates 175 # an android device. See go/emu-dev for more 176 # 2. A system-image. This is the android release we wish to run on the 177 # emulated hardware. 178 create_gf_parser = subparsers.add_parser(CMD_CREATE_GOLDFISH) 179 create_gf_parser.required = False 180 create_gf_parser.set_defaults(which=CMD_CREATE_GOLDFISH) 181 create_gf_parser.add_argument( 182 "--emulator-build-id", 183 type=str, 184 dest="emulator_build_id", 185 required=False, 186 help="Emulator build used to run the images. e.g. 4669466.") 187 create_gf_parser.add_argument( 188 "--emulator-branch", 189 type=str, 190 dest="emulator_branch", 191 required=False, 192 help="Emulator build branch name, e.g. aosp-emu-master-dev. If specified" 193 " without emulator-build-id, the last green build will be used.") 194 create_gf_parser.add_argument( 195 "--emulator-build-target", 196 dest="emulator_build_target", 197 required=False, 198 help="Emulator build target used to run the images. e.g. " 199 "emulator-linux_x64_nolocationui.") 200 create_gf_parser.add_argument( 201 "--base-image", 202 type=str, 203 dest="base_image", 204 required=False, 205 help="Name of the goldfish base image to be used to create the instance. " 206 "This will override stable_goldfish_host_image_name from config. " 207 "e.g. emu-dev-cts-061118") 208 create_gf_parser.add_argument( 209 "--tags", 210 dest="tags", 211 nargs="*", 212 required=False, 213 default=None, 214 help="Tags to be set on to the created instance. e.g. https-server.") 215 # Arguments in old format 216 create_gf_parser.add_argument( 217 "--emulator_build_id", 218 type=str, 219 dest="emulator_build_id", 220 required=False, 221 help=argparse.SUPPRESS) 222 create_gf_parser.add_argument( 223 "--emulator_branch", 224 type=str, 225 dest="emulator_branch", 226 required=False, 227 help=argparse.SUPPRESS) 228 create_gf_parser.add_argument( 229 "--base_image", 230 type=str, 231 dest="base_image", 232 required=False, 233 help=argparse.SUPPRESS) 234 235 create_args.AddCommonCreateArgs(create_gf_parser) 236 subparser_list.append(create_gf_parser) 237 238 # Command "create" 239 subparser_list.append(create_args.GetCreateArgParser(subparsers)) 240 241 # Command "setup" 242 subparser_list.append(setup_args.GetSetupArgParser(subparsers)) 243 244 # Command "delete" 245 subparser_list.append(delete_args.GetDeleteArgParser(subparsers)) 246 247 # Command "list" 248 subparser_list.append(list_args.GetListArgParser(subparsers)) 249 250 # Command "reconnect" 251 subparser_list.append(reconnect_args.GetReconnectArgParser(subparsers)) 252 253 # Command "restart" 254 subparser_list.append(restart_args.GetRestartArgParser(subparsers)) 255 256 # Command "powerwash" 257 subparser_list.append(powerwash_args.GetPowerwashArgParser(subparsers)) 258 259 # Command "pull" 260 subparser_list.append(pull_args.GetPullArgParser(subparsers)) 261 262 # Command "hostcleanup" 263 subparser_list.append(hostcleanup_args.GetHostcleanupArgParser(subparsers)) 264 265 # Add common arguments. 266 for subparser in subparser_list: 267 acloud_common.AddCommonArguments(subparser) 268 269 support_args = acloud_cmds + DEFAULT_SUPPORT_ARGS 270 if not args or args[0] not in support_args: 271 parser.print_help() 272 sys.exit(constants.EXIT_BY_WRONG_CMD) 273 274 return parser.parse_known_args(args) 275 276 277# pylint: disable=too-many-branches 278def _VerifyArgs(parsed_args): 279 """Verify args. 280 281 Args: 282 parsed_args: Parsed args. 283 284 Raises: 285 errors.CommandArgError: If args are invalid. 286 errors.UnsupportedCreateArgs: When a create arg is specified but 287 unsupported for a particular avd type. 288 (e.g. --system-build-id for gf) 289 """ 290 if parsed_args.which == create_args.CMD_CREATE: 291 create_args.VerifyArgs(parsed_args) 292 if parsed_args.which == setup_args.CMD_SETUP: 293 setup_args.VerifyArgs(parsed_args) 294 if parsed_args.which == CMD_CREATE_GOLDFISH: 295 if not parsed_args.emulator_build_id and not parsed_args.build_id and ( 296 not parsed_args.emulator_branch and not parsed_args.branch): 297 raise errors.CommandArgError( 298 "Must specify either --build-id or --branch or " 299 "--emulator-branch or --emulator-build-id") 300 if not parsed_args.build_target: 301 raise errors.CommandArgError("Must specify --build-target") 302 if (parsed_args.system_branch 303 or parsed_args.system_build_id 304 or parsed_args.system_build_target): 305 raise errors.UnsupportedCreateArgs( 306 "--system-* args are not supported for AVD type: %s" 307 % constants.TYPE_GF) 308 309 if parsed_args.which in [create_args.CMD_CREATE, CMD_CREATE_GOLDFISH]: 310 if (parsed_args.serial_log_file 311 and not parsed_args.serial_log_file.endswith(".tar.gz")): 312 raise errors.CommandArgError( 313 "--serial-log-file must ends with .tar.gz") 314 315 316def _ValidateAuthFile(cfg): 317 """Check if the authentication file exist. 318 319 Args: 320 cfg: AcloudConfig object. 321 """ 322 auth_file = os.path.join(os.path.expanduser("~"), cfg.creds_cache_file) 323 if not os.path.exists(auth_file): 324 print("Notice: Acloud will bring up browser to proceed authentication. " 325 "For cloudtop, please run in remote desktop.") 326 327 328def _ParsingConfig(args, cfg): 329 """Parse config to check if missing any field. 330 331 Args: 332 args: Namespace object from argparse.parse_args. 333 cfg: AcloudConfig object. 334 335 Returns: 336 error message about list of missing config fields. 337 """ 338 missing_fields = [] 339 if (args.which == create_args.CMD_CREATE and 340 args.local_instance is None and not args.remote_host): 341 missing_fields = cfg.GetMissingFields(_CREATE_REQUIRE_FIELDS) 342 if missing_fields: 343 return (f"Config file ({config.GetUserConfigPath(args.config_file)}) " 344 f"missing required fields: {missing_fields}, please add these " 345 "fields or reset config file. For reset config information: " 346 "go/acloud-googler-setup#reset-configuration") 347 return None 348 349 350def _SetupLogging(log_file, verbose): 351 """Setup logging. 352 353 This function define the logging policy in below manners. 354 - without -v , -vv ,--log-file: 355 Only display critical log and print() message on screen. 356 357 - with -v: 358 Display INFO log and set StreamHandler to acloud parent logger to turn on 359 ONLY acloud modules logging.(silence all 3p libraries) 360 361 - with -vv: 362 Display INFO/DEBUG log and set StreamHandler to root logger to turn on all 363 acloud modules and 3p libraries logging. 364 365 - with --log-file. 366 Dump logs to FileHandler with DEBUG level. 367 368 Args: 369 log_file: String, if not None, dump the log to log file. 370 verbose: Int, if verbose = 1(-v), log at INFO level and turn on 371 logging on libraries to a StreamHandler. 372 If verbose = 2(-vv), log at DEBUG level and turn on logging on 373 all libraries and 3rd party libraries to a StreamHandler. 374 """ 375 # Define logging level and hierarchy by verbosity. 376 shandler_level = None 377 logger = None 378 if verbose == 0: 379 shandler_level = logging.CRITICAL 380 logger = logging.getLogger(ACLOUD_LOGGER) 381 elif verbose == 1: 382 shandler_level = logging.INFO 383 logger = logging.getLogger(ACLOUD_LOGGER) 384 elif verbose > 1: 385 shandler_level = logging.DEBUG 386 logger = logging.getLogger() 387 388 # Add StreamHandler by default. 389 shandler = logging.StreamHandler() 390 shandler.setFormatter(logging.Formatter(LOGGING_FMT)) 391 shandler.setLevel(shandler_level) 392 logger.addHandler(shandler) 393 # Set the default level to DEBUG, the other handlers will handle 394 # their own levels via the args supplied (-v and --log-file). 395 logger.setLevel(logging.DEBUG) 396 397 # Add FileHandler if log_file is provided. 398 if log_file: 399 fhandler = logging.FileHandler(filename=log_file) 400 fhandler.setFormatter(logging.Formatter(LOGGING_FMT)) 401 fhandler.setLevel(logging.DEBUG) 402 logger.addHandler(fhandler) 403 404 405def main(argv=None): 406 """Main entry. 407 408 Args: 409 argv: A list of system arguments. 410 411 Returns: 412 Job status: Integer, 0 if success. None-zero if fails. 413 Stack trace: String of errors. 414 """ 415 args, unknown_args = _ParseArgs(argv) 416 _SetupLogging(args.log_file, args.verbose) 417 _VerifyArgs(args) 418 _LOGGER.info("Acloud version: %s", config.GetVersion()) 419 420 cfg = config.GetAcloudConfig(args) 421 parsing_config_error = _ParsingConfig(args, cfg) 422 _ValidateAuthFile(cfg) 423 # TODO: Move this check into the functions it is actually needed. 424 # Check access. 425 # device_driver.CheckAccess(cfg) 426 427 reporter = None 428 if parsing_config_error: 429 reporter = report.Report(command=args.which) 430 reporter.UpdateFailure(parsing_config_error, 431 constants.ACLOUD_CONFIG_ERROR) 432 elif unknown_args: 433 reporter = report.Report(command=args.which) 434 reporter.UpdateFailure( 435 "unrecognized arguments: %s" % ",".join(unknown_args), 436 constants.ACLOUD_UNKNOWN_ARGS_ERROR) 437 elif args.which == create_args.CMD_CREATE: 438 reporter = create.Run(args) 439 elif args.which == CMD_CREATE_GOLDFISH: 440 reporter = create_goldfish_action.CreateDevices( 441 cfg=cfg, 442 build_target=args.build_target, 443 branch=args.branch, 444 build_id=args.build_id, 445 emulator_build_id=args.emulator_build_id, 446 emulator_branch=args.emulator_branch, 447 emulator_build_target=args.emulator_build_target, 448 kernel_build_id=args.kernel_build_id, 449 kernel_branch=args.kernel_branch, 450 kernel_build_target=args.kernel_build_target, 451 gpu=args.gpu, 452 num=args.num, 453 serial_log_file=args.serial_log_file, 454 autoconnect=args.autoconnect, 455 tags=args.tags, 456 report_internal_ip=args.report_internal_ip, 457 boot_timeout_secs=args.boot_timeout_secs) 458 elif args.which == delete_args.CMD_DELETE: 459 reporter = delete.Run(args) 460 elif args.which == list_args.CMD_LIST: 461 list_instances.Run(args) 462 elif args.which == reconnect_args.CMD_RECONNECT: 463 reconnect.Run(args) 464 elif args.which == restart_args.CMD_RESTART: 465 reporter = restart.Run(args) 466 elif args.which == powerwash_args.CMD_POWERWASH: 467 reporter = powerwash.Run(args) 468 elif args.which == pull_args.CMD_PULL: 469 reporter = pull.Run(args) 470 elif args.which == setup_args.CMD_SETUP: 471 setup.Run(args) 472 elif args.which == hostcleanup_args.CMD_HOSTCLEANUP: 473 hostcleanup.Run(args) 474 else: 475 error_msg = "Invalid command %s" % args.which 476 sys.stderr.write(error_msg) 477 return constants.EXIT_BY_WRONG_CMD, error_msg 478 479 if reporter and args.report_file: 480 reporter.Dump(args.report_file) 481 if reporter and reporter.errors: 482 error_msg = "\n".join(reporter.errors) 483 help_msg = _CONTACT_INFO 484 if reporter.data.get(constants.ERROR_LOG_FOLDER): 485 help_msg += _LOG_INFO % reporter.data.get(constants.ERROR_LOG_FOLDER) 486 sys.stderr.write("Encountered the following errors:\n%s\n\n%s.\n" % 487 (error_msg, help_msg)) 488 return constants.EXIT_BY_FAIL_REPORT, error_msg 489 return constants.EXIT_SUCCESS, NO_ERROR_MESSAGE 490 491 492if __name__ == "__main__": 493 EXIT_CODE = None 494 EXCEPTION_STACKTRACE = None 495 EXCEPTION_LOG = None 496 LOG_METRICS = metrics.LogUsage(sys.argv[1:]) 497 try: 498 EXIT_CODE, EXCEPTION_STACKTRACE = main(sys.argv[1:]) 499 except Exception as e: 500 EXIT_CODE = constants.EXIT_BY_ERROR 501 EXCEPTION_STACKTRACE = traceback.format_exc() 502 EXCEPTION_LOG = str(e) 503 sys.stderr.write("Exception: %s" % (EXCEPTION_STACKTRACE)) 504 505 # Log Exit event here to calculate the consuming time. 506 if LOG_METRICS: 507 metrics.LogExitEvent(EXIT_CODE, 508 stacktrace=EXCEPTION_STACKTRACE, 509 logs=EXCEPTION_LOG) 510 sys.exit(EXIT_CODE) 511