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