xref: /aosp_15_r20/tools/acloud/public/acloud_main.py (revision 800a58d989c669b8eb8a71d8df53b1ba3d411444)
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