Home | History | Annotate | Download | only in public
      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.
     16 
     17 r"""Cloud Android Driver.
     18 
     19 This CLI manages google compute engine project for android devices.
     20 
     21 - Prerequisites:
     22   See: go/acloud-manual
     23 
     24 - Configuration:
     25   The script takes a required configuration file, which should look like
     26      <Start of the file>
     27      # If using service account
     28      service_account_name: "your_account@developer.gserviceaccount.com"
     29      service_account_private_key_path: "/path/to/your-project.p12"
     30 
     31      # If using OAuth2 authentication flow
     32      client_id: <client id created in the project>
     33      client_secret: <client secret for the client id>
     34 
     35      # Optional
     36      ssh_private_key_path: "~/.ssh/acloud_rsa"
     37      ssh_public_key_path: "~/.ssh/acloud_rsa.pub"
     38      orientation: "portrait"
     39      resolution: "800x1280x32x213"
     40      network: "default"
     41      machine_type: "n1-standard-1"
     42      extra_data_disk_size_gb: 10  # 4G or 10G
     43 
     44      # Required
     45      project: "your-project"
     46      zone: "us-central1-f"
     47      storage_bucket_name: "your_google_storage_bucket_name"
     48      <End of the file>
     49 
     50   Save it at /path/to/acloud.config
     51 
     52 - Example calls:
     53   - Create two instances:
     54   $ acloud.par create
     55     --build_target gce_x86_phone-userdebug_fastbuild3c_linux \
     56     --build_id 3744001 --num 2 --config_file /path/to/acloud.config \
     57     --report_file /tmp/acloud_report.json --log_file /tmp/acloud.log
     58 
     59   - Delete two instances:
     60   $ acloud.par delete --instance_names
     61     ins-b638cdba-3744001-gce-x86-phone-userdebug-fastbuild3c-linux
     62     --config_file /path/to/acloud.config
     63     --report_file /tmp/acloud_report.json --log_file /tmp/acloud.log
     64 """
     65 import argparse
     66 import getpass
     67 import logging
     68 import os
     69 import sys
     70 
     71 from acloud.internal import constants
     72 from acloud.public import acloud_common
     73 from acloud.public import config
     74 from acloud.public import device_driver
     75 from acloud.public import errors
     76 
     77 LOGGING_FMT = "%(asctime)s |%(levelname)s| %(module)s:%(lineno)s| %(message)s"
     78 LOGGER_NAME = "acloud_main"
     79 
     80 # Commands
     81 CMD_CREATE = "create"
     82 CMD_DELETE = "delete"
     83 CMD_CLEANUP = "cleanup"
     84 CMD_SSHKEY = "project_sshkey"
     85 
     86 
     87 def _ParseArgs(args):
     88     """Parse args.
     89 
     90     Args:
     91         args: Argument list passed from main.
     92 
     93     Returns:
     94         Parsed args.
     95     """
     96     usage = ",".join([CMD_CREATE, CMD_DELETE, CMD_CLEANUP, CMD_SSHKEY])
     97     parser = argparse.ArgumentParser(
     98         description=__doc__,
     99         formatter_class=argparse.RawDescriptionHelpFormatter,
    100         usage="%(prog)s {" + usage + "} ...")
    101     subparsers = parser.add_subparsers()
    102     subparser_list = []
    103 
    104     # Command "create"
    105     create_parser = subparsers.add_parser(CMD_CREATE)
    106     create_parser.required = False
    107     create_parser.set_defaults(which=CMD_CREATE)
    108     create_parser.add_argument(
    109         "--build_target",
    110         type=str,
    111         dest="build_target",
    112         help="Android build target, e.g. gce_x86-userdebug, "
    113         "or short names: phone, tablet, or tablet_mobile.")
    114     create_parser.add_argument(
    115         "--branch",
    116         type=str,
    117         dest="branch",
    118         help="Android branch, e.g. mnc-dev or git_mnc-dev")
    119     # TODO(fdeng): Support HEAD (the latest build)
    120     create_parser.add_argument("--build_id",
    121                                type=str,
    122                                dest="build_id",
    123                                help="Android build id, e.g. 2145099, P2804227")
    124     create_parser.add_argument(
    125         "--spec",
    126         type=str,
    127         dest="spec",
    128         required=False,
    129         help="The name of a pre-configured device spec that we are "
    130         "going to use. Choose from: %s" % ", ".join(constants.SPEC_NAMES))
    131     create_parser.add_argument("--num",
    132                                type=int,
    133                                dest="num",
    134                                required=False,
    135                                default=1,
    136                                help="Number of instances to create.")
    137     create_parser.add_argument(
    138         "--gce_image",
    139         type=str,
    140         dest="gce_image",
    141         required=False,
    142         help="Name of an existing compute engine image to reuse.")
    143     create_parser.add_argument("--local_disk_image",
    144                                type=str,
    145                                dest="local_disk_image",
    146                                required=False,
    147                                help="Path to a local disk image to use, "
    148                                "e.g /tmp/avd-system.tar.gz")
    149     create_parser.add_argument(
    150         "--no_cleanup",
    151         dest="no_cleanup",
    152         default=False,
    153         action="store_true",
    154         help="Do not clean up temporary disk image and compute engine image. "
    155         "For debugging purposes.")
    156     create_parser.add_argument(
    157         "--serial_log_file",
    158         type=str,
    159         dest="serial_log_file",
    160         required=False,
    161         help="Path to a *tar.gz file where serial logs will be saved "
    162         "when a device fails on boot.")
    163     create_parser.add_argument(
    164         "--logcat_file",
    165         type=str,
    166         dest="logcat_file",
    167         required=False,
    168         help="Path to a *tar.gz file where logcat logs will be saved "
    169         "when a device fails on boot.")
    170 
    171     subparser_list.append(create_parser)
    172 
    173     # Command "Delete"
    174     delete_parser = subparsers.add_parser(CMD_DELETE)
    175     delete_parser.required = False
    176     delete_parser.set_defaults(which=CMD_DELETE)
    177     delete_parser.add_argument(
    178         "--instance_names",
    179         dest="instance_names",
    180         nargs="+",
    181         required=True,
    182         help="The names of the instances that need to delete, "
    183         "separated by spaces, e.g. --instance_names instance-1 instance-2")
    184     subparser_list.append(delete_parser)
    185 
    186     # Command "cleanup"
    187     cleanup_parser = subparsers.add_parser(CMD_CLEANUP)
    188     cleanup_parser.required = False
    189     cleanup_parser.set_defaults(which=CMD_CLEANUP)
    190     cleanup_parser.add_argument(
    191         "--expiration_mins",
    192         type=int,
    193         dest="expiration_mins",
    194         required=True,
    195         help="Garbage collect all gce instances, gce images, cached disk "
    196         "images that are older than |expiration_mins|.")
    197     subparser_list.append(cleanup_parser)
    198 
    199     # Command "project_sshkey"
    200     sshkey_parser = subparsers.add_parser(CMD_SSHKEY)
    201     sshkey_parser.required = False
    202     sshkey_parser.set_defaults(which=CMD_SSHKEY)
    203     sshkey_parser.add_argument(
    204         "--user",
    205         type=str,
    206         dest="user",
    207         default=getpass.getuser(),
    208         help="The user name which the sshkey belongs to, default to: %s." %
    209         getpass.getuser())
    210     sshkey_parser.add_argument(
    211         "--ssh_rsa_path",
    212         type=str,
    213         dest="ssh_rsa_path",
    214         required=True,
    215         help="Absolute path to the file that contains the public rsa key "
    216              "that will be added as project-wide ssh key.")
    217     subparser_list.append(sshkey_parser)
    218 
    219     # Add common arguments.
    220     for p in subparser_list:
    221         acloud_common.AddCommonArguments(p)
    222 
    223     return parser.parse_args(args)
    224 
    225 
    226 def _TranslateAlias(parsed_args):
    227     """Translate alias to Launch Control compatible values.
    228 
    229     This method translates alias to Launch Control compatible values.
    230      - branch: "git_" prefix will be added if branch name doesn't have it.
    231      - build_target: For example, "phone" will be translated to full target
    232                           name "git_x86_phone-userdebug",
    233 
    234     Args:
    235         parsed_args: Parsed args.
    236 
    237     Returns:
    238         Parsed args with its values being translated.
    239     """
    240     if parsed_args.which == CMD_CREATE:
    241         if (parsed_args.branch and
    242                 not parsed_args.branch.startswith(constants.BRANCH_PREFIX)):
    243             parsed_args.branch = constants.BRANCH_PREFIX + parsed_args.branch
    244         parsed_args.build_target = constants.BUILD_TARGET_MAPPING.get(
    245             parsed_args.build_target, parsed_args.build_target)
    246     return parsed_args
    247 
    248 
    249 def _VerifyArgs(parsed_args):
    250     """Verify args.
    251 
    252     Args:
    253         parsed_args: Parsed args.
    254 
    255     Raises:
    256         errors.CommandArgError: If args are invalid.
    257     """
    258     if parsed_args.which == CMD_CREATE:
    259         if (parsed_args.spec and parsed_args.spec not in constants.SPEC_NAMES):
    260             raise errors.CommandArgError(
    261                 "%s is not valid. Choose from: %s" %
    262                 (parsed_args.spec, ", ".join(constants.SPEC_NAMES)))
    263         if not ((parsed_args.build_id and parsed_args.build_target) or
    264                 parsed_args.gce_image or parsed_args.local_disk_image):
    265             raise errors.CommandArgError(
    266                 "At least one of the following should be specified: "
    267                 "--build_id and --build_target, or --gce_image, or "
    268                 "--local_disk_image.")
    269         if bool(parsed_args.build_id) != bool(parsed_args.build_target):
    270             raise errors.CommandArgError(
    271                 "Must specify --build_id and --build_target at the same time.")
    272         if (parsed_args.serial_log_file and
    273                 not parsed_args.serial_log_file.endswith(".tar.gz")):
    274             raise errors.CommandArgError(
    275                 "--serial_log_file must ends with .tar.gz")
    276         if (parsed_args.logcat_file and
    277                 not parsed_args.logcat_file.endswith(".tar.gz")):
    278             raise errors.CommandArgError(
    279                 "--logcat_file must ends with .tar.gz")
    280 
    281 
    282 def _SetupLogging(log_file, verbose, very_verbose):
    283     """Setup logging.
    284 
    285     Args:
    286         log_file: path to log file.
    287         verbose: If True, log at DEBUG level, otherwise log at INFO level.
    288         very_verbose: If True, log at DEBUG level and turn on logging on
    289                       all libraries. Take take precedence over |verbose|.
    290     """
    291     if very_verbose:
    292         logger = logging.getLogger()
    293     else:
    294         logger = logging.getLogger(LOGGER_NAME)
    295 
    296     logging_level = logging.DEBUG if verbose or very_verbose else logging.INFO
    297     logger.setLevel(logging_level)
    298 
    299     if not log_file:
    300         handler = logging.StreamHandler()
    301     else:
    302         handler = logging.FileHandler(filename=log_file)
    303     log_formatter = logging.Formatter(LOGGING_FMT)
    304     handler.setFormatter(log_formatter)
    305     logger.addHandler(handler)
    306 
    307 
    308 def main(argv):
    309     """Main entry.
    310 
    311     Args:
    312         argv: A list of system arguments.
    313 
    314     Returns:
    315         0 if success. None-zero if fails.
    316     """
    317     args = _ParseArgs(argv)
    318     _SetupLogging(args.log_file, args.verbose, args.very_verbose)
    319     args = _TranslateAlias(args)
    320     _VerifyArgs(args)
    321 
    322     config_mgr = config.AcloudConfigManager(args.config_file)
    323     cfg = config_mgr.Load()
    324     cfg.OverrideWithArgs(args)
    325 
    326     # Check access.
    327     device_driver.CheckAccess(cfg)
    328 
    329     if args.which == CMD_CREATE:
    330         report = device_driver.CreateAndroidVirtualDevices(
    331             cfg,
    332             args.build_target,
    333             args.build_id,
    334             args.num,
    335             args.gce_image,
    336             args.local_disk_image,
    337             cleanup=not args.no_cleanup,
    338             serial_log_file=args.serial_log_file,
    339             logcat_file=args.logcat_file)
    340     elif args.which == CMD_DELETE:
    341         report = device_driver.DeleteAndroidVirtualDevices(cfg,
    342                                                            args.instance_names)
    343     elif args.which == CMD_CLEANUP:
    344         report = device_driver.Cleanup(cfg, args.expiration_mins)
    345     elif args.which == CMD_SSHKEY:
    346         report = device_driver.AddSshRsa(cfg, args.user, args.ssh_rsa_path)
    347     else:
    348         sys.stderr.write("Invalid command %s" % args.which)
    349         return 2
    350 
    351     report.Dump(args.report_file)
    352     if report.errors:
    353         msg = "\n".join(report.errors)
    354         sys.stderr.write("Encountered the following errors:\n%s\n" % msg)
    355         return 1
    356     return 0
    357 
    358 
    359 if __name__ == "__main__":
    360     main(sys.argv[1:])
    361