initial commit of acloud for open sourcing
The Cloud Android Driver Binaries (namely, acloud) in this project
provide the standard APIs to access and control Cloud Android devices
(i.e., Android Virtual Devices on Google Compute Engine) instantiated
by using the Android source code (e.g., device/google/gce* projects).
No code change required in the initial commit which is to track
all the changes submitted after the initial commit.
Unit tests are not part of this initial commit and thus will be
submitted as the second commit due to their current dependencies
Test: no build rule defined for python yet
Change-Id: Ib6aaadf33fa110f4532ba2d5b7be91e8ddc632a9
diff --git a/public/acloud.py b/public/acloud.py
new file mode 100755
index 0000000..5badcef
--- /dev/null
+++ b/public/acloud.py
@@ -0,0 +1,351 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Cloud Android Driver.
+
+This CLI manages google compute engine project for android devices.
+
+- Prerequisites:
+ See: go/acloud-manual
+
+- Configuration:
+ The script takes a required configuration file, which should look like
+ <Start of the file>
+ # If using service account
+ service_account_name: "your_account@developer.gserviceaccount.com"
+ service_account_private_key_path: "/path/to/your-project.p12"
+
+ # If using OAuth2 authentication flow
+ client_id: <client id created in the project>
+ client_secret: <client secret for the client id>
+
+ # Optional
+ ssh_private_key_path: ""
+ orientation: "portrait"
+ resolution: "800x1280x32x213"
+ network: "default"
+ machine_type: "n1-standard-1"
+ extra_data_disk_size_gb: 10 # 4G or 10G
+
+ # Required
+ project: "your-project"
+ zone: "us-central1-f"
+ storage_bucket_name: "your_google_storage_bucket_name"
+ <End of the file>
+
+ Save it at /path/to/acloud.config
+
+- Example calls:
+ - Create two instances:
+ $ acloud.par create
+ --build_target gce_x86_phone-userdebug
+ --build_id 2305447 --num 2 --config_file /path/to/acloud.config
+ --report_file /tmp/acloud_report.json --log_file /tmp/acloud.log
+
+ - Delete two instances:
+ $ acloud.par delete --instance_names
+ gce-x86-userdebug-2272605-43f9b2c6 gce-x86-userdebug-2272605-20f93a5
+ --config_file /path/to/acloud.config
+ --report_file /tmp/acloud_report.json --log_file /tmp/acloud.log
+"""
+import argparse
+import getpass
+import logging
+import os
+import sys
+
+from acloud.internal import constants
+from acloud.public import acloud_common
+from acloud.public import config
+from acloud.public import device_driver
+from acloud.public import errors
+
+LOGGING_FMT = "%(asctime)s |%(levelname)s| %(module)s:%(lineno)s| %(message)s"
+LOGGER_NAME = "google3.cloud.android.driver"
+
+# Commands
+CMD_CREATE = "create"
+CMD_DELETE = "delete"
+CMD_CLEANUP = "cleanup"
+CMD_SSHKEY = "sshkey"
+
+
+def _ParseArgs(args):
+ """Parse args.
+
+ Args:
+ args: Argument list passed from main.
+
+ Returns:
+ Parsed args.
+ """
+ usage = ",".join([CMD_CREATE, CMD_DELETE, CMD_CLEANUP, CMD_SSHKEY])
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ usage="%(prog)s {" + usage + "} ...")
+ subparsers = parser.add_subparsers()
+ subparser_list = []
+
+ # Command "create"
+ create_parser = subparsers.add_parser(CMD_CREATE)
+ create_parser.required = False
+ create_parser.set_defaults(which=CMD_CREATE)
+ create_parser.add_argument(
+ "--build_target",
+ type=str,
+ dest="build_target",
+ help="Android build target, e.g. gce_x86-userdebug, "
+ "or short names: phone, tablet, or tablet_mobile.")
+ create_parser.add_argument(
+ "--branch",
+ type=str,
+ dest="branch",
+ help="Android branch, e.g. mnc-dev or git_mnc-dev")
+ # TODO(fdeng): Support HEAD (the latest build)
+ create_parser.add_argument("--build_id",
+ type=str,
+ dest="build_id",
+ help="Android build id, e.g. 2145099, P2804227")
+ create_parser.add_argument(
+ "--spec",
+ type=str,
+ dest="spec",
+ required=False,
+ help="The name of a pre-configured device spec that we are "
+ "going to use. Choose from: %s" % ", ".join(constants.SPEC_NAMES))
+ create_parser.add_argument("--num",
+ type=int,
+ dest="num",
+ required=False,
+ default=1,
+ help="Number of instances to create.")
+ create_parser.add_argument(
+ "--gce_image",
+ type=str,
+ dest="gce_image",
+ required=False,
+ help="Name of an existing compute engine image to reuse.")
+ create_parser.add_argument("--local_disk_image",
+ type=str,
+ dest="local_disk_image",
+ required=False,
+ help="Path to a local disk image to use, "
+ "e.g /tmp/avd-system.tar.gz")
+ create_parser.add_argument(
+ "--no_cleanup",
+ dest="no_cleanup",
+ default=False,
+ action="store_true",
+ help="Do not clean up temporary disk image and compute engine image. "
+ "For debugging purposes.")
+ create_parser.add_argument(
+ "--serial_log_file",
+ type=str,
+ dest="serial_log_file",
+ required=False,
+ help="Path to a *tar.gz file where serial logs will be saved "
+ "when a device fails on boot.")
+ create_parser.add_argument(
+ "--logcat_file",
+ type=str,
+ dest="logcat_file",
+ required=False,
+ help="Path to a *tar.gz file where logcat logs will be saved "
+ "when a device fails on boot.")
+
+ subparser_list.append(create_parser)
+
+ # Command "Delete"
+ delete_parser = subparsers.add_parser(CMD_DELETE)
+ delete_parser.required = False
+ delete_parser.set_defaults(which=CMD_DELETE)
+ delete_parser.add_argument(
+ "--instance_names",
+ dest="instance_names",
+ nargs="+",
+ required=True,
+ help="The names of the instances that need to delete, "
+ "separated by spaces, e.g. --instance_names instance-1 instance-2")
+ subparser_list.append(delete_parser)
+
+ # Command "cleanup"
+ cleanup_parser = subparsers.add_parser(CMD_CLEANUP)
+ cleanup_parser.required = False
+ cleanup_parser.set_defaults(which=CMD_CLEANUP)
+ cleanup_parser.add_argument(
+ "--expiration_mins",
+ type=int,
+ dest="expiration_mins",
+ required=True,
+ help="Garbage collect all gce instances, gce images, cached disk "
+ "images that are older than |expiration_mins|.")
+ subparser_list.append(cleanup_parser)
+
+ # Command "sshkey"
+ sshkey_parser = subparsers.add_parser(CMD_SSHKEY)
+ sshkey_parser.required = False
+ sshkey_parser.set_defaults(which=CMD_SSHKEY)
+ sshkey_parser.add_argument(
+ "--user",
+ type=str,
+ dest="user",
+ default=getpass.getuser(),
+ help="The user name which the sshkey belongs to, default to: %s." %
+ getpass.getuser())
+ sshkey_parser.add_argument(
+ "--ssh_rsa_path",
+ type=str,
+ dest="ssh_rsa_path",
+ required=True,
+ help="Absolute path to the file that contains the public rsa key.")
+ subparser_list.append(sshkey_parser)
+
+ # Add common arguments.
+ for p in subparser_list:
+ acloud_common.AddCommonArguments(p)
+
+ return parser.parse_args(args)
+
+
+def _TranslateAlias(parsed_args):
+ """Translate alias to Launch Control compatible values.
+
+ This method translates alias to Launch Control compatible values.
+ - branch: "git_" prefix will be added if branch name doesn't have it.
+ - build_target: For example, "phone" will be translated to full target
+ name "git_x86_phone-userdebug",
+
+ Args:
+ parsed_args: Parsed args.
+
+ Returns:
+ Parsed args with its values being translated.
+ """
+ if parsed_args.which == CMD_CREATE:
+ if (parsed_args.branch and
+ not parsed_args.branch.startswith(constants.BRANCH_PREFIX)):
+ parsed_args.branch = constants.BRANCH_PREFIX + parsed_args.branch
+ parsed_args.build_target = constants.BUILD_TARGET_MAPPING.get(
+ parsed_args.build_target, parsed_args.build_target)
+ return parsed_args
+
+
+def _VerifyArgs(parsed_args):
+ """Verify args.
+
+ Args:
+ parsed_args: Parsed args.
+
+ Raises:
+ errors.CommandArgError: If args are invalid.
+ """
+ if parsed_args.which == CMD_CREATE:
+ if (parsed_args.spec and parsed_args.spec not in constants.SPEC_NAMES):
+ raise errors.CommandArgError(
+ "%s is not valid. Choose from: %s" %
+ (parsed_args.spec, ", ".join(constants.SPEC_NAMES)))
+ if not ((parsed_args.build_id and parsed_args.build_target) or
+ parsed_args.gce_image or parsed_args.local_disk_image):
+ raise errors.CommandArgError(
+ "At least one of the following should be specified: "
+ "--build_id and --build_target, or --gce_image, or "
+ "--local_disk_image.")
+ if bool(parsed_args.build_id) != bool(parsed_args.build_target):
+ raise errors.CommandArgError(
+ "Must specify --build_id and --build_target at the same time.")
+ if (parsed_args.serial_log_file and
+ not parsed_args.serial_log_file.endswith(".tar.gz")):
+ raise errors.CommandArgError(
+ "--serial_log_file must ends with .tar.gz")
+ if (parsed_args.logcat_file and
+ not parsed_args.logcat_file.endswith(".tar.gz")):
+ raise errors.CommandArgError(
+ "--logcat_file must ends with .tar.gz")
+
+
+def _SetupLogging(log_file, verbose, very_verbose):
+ """Setup logging.
+
+ Args:
+ log_file: path to log file.
+ verbose: If True, log at DEBUG level, otherwise log at INFO level.
+ very_verbose: If True, log at DEBUG level and turn on logging on
+ all libraries. Take take precedence over |verbose|.
+ """
+ if very_verbose:
+ logger = logging.getLogger()
+ else:
+ logger = logging.getLogger(LOGGER_NAME)
+
+ logging_level = logging.DEBUG if verbose or very_verbose else logging.INFO
+ logger.setLevel(logging_level)
+
+ if not log_file:
+ handler = logging.StreamHandler()
+ else:
+ handler = logging.FileHandler(filename=log_file)
+ log_formatter = logging.Formatter(LOGGING_FMT)
+ handler.setFormatter(log_formatter)
+ logger.addHandler(handler)
+
+
+def main(argv):
+ """Main entry.
+
+ Args:
+ argv: A list of system arguments.
+
+ Returns:
+ 0 if success. None-zero if fails.
+ """
+ args = _ParseArgs(argv)
+ _SetupLogging(args.log_file, args.verbose, args.very_verbose)
+ args = _TranslateAlias(args)
+ _VerifyArgs(args)
+
+ config_mgr = config.AcloudConfigManager(args.config_file)
+ cfg = config_mgr.Load()
+ cfg.OverrideWithArgs(args)
+
+ if args.which == CMD_CREATE:
+ report = device_driver.CreateAndroidVirtualDevices(
+ cfg,
+ args.build_target,
+ args.build_id,
+ args.num,
+ args.gce_image,
+ args.local_disk_image,
+ cleanup=not args.no_cleanup,
+ serial_log_file=args.serial_log_file,
+ logcat_file=args.logcat_file)
+ elif args.which == CMD_DELETE:
+ report = device_driver.DeleteAndroidVirtualDevices(cfg,
+ args.instance_names)
+ elif args.which == CMD_CLEANUP:
+ report = device_driver.Cleanup(cfg, args.expiration_mins)
+ elif args.which == CMD_SSHKEY:
+ report = device_driver.AddSshRsa(cfg, args.user, args.ssh_rsa_path)
+ else:
+ sys.stderr.write("Invalid command %s" % args.which)
+ return 2
+
+ report.Dump(args.report_file)
+ if report.errors:
+ msg = "\n".join(report.errors)
+ sys.stderr.write("Encountered the following errors:\n%s\n" % msg)
+ return 1
+ return 0