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