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