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