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 17r"""Cloud Android Driver. 18 19This 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""" 65import argparse 66import getpass 67import logging 68import os 69import sys 70 71from acloud.internal import constants 72from acloud.public import acloud_common 73from acloud.public import config 74from acloud.public import device_driver 75from acloud.public import errors 76 77LOGGING_FMT = "%(asctime)s |%(levelname)s| %(module)s:%(lineno)s| %(message)s" 78LOGGER_NAME = "acloud_main" 79 80# Commands 81CMD_CREATE = "create" 82CMD_DELETE = "delete" 83CMD_CLEANUP = "cleanup" 84CMD_SSHKEY = "project_sshkey" 85 86 87def _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 226def _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 249def _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 282def _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 308def 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 359if __name__ == "__main__": 360 main(sys.argv[1:]) 361