blob: d00cd91be3e5df5cb88338efd9c41758a80a2dae [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
Fang Dengfed6a6f2017-03-01 18:27:28 -080017r"""Cloud Android Driver.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070018
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
Fang Deng69498c32017-03-02 14:29:30 -080036 ssh_private_key_path: "~/.ssh/acloud_rsa"
37 ssh_public_key_path: "~/.ssh/acloud_rsa.pub"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070038 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
Fang Dengfed6a6f2017-03-01 18:27:28 -080055 --build_target gce_x86_phone-userdebug_fastbuild3c_linux \
56 --build_id 3744001 --num 2 --config_file /path/to/acloud.config \
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070057 --report_file /tmp/acloud_report.json --log_file /tmp/acloud.log
58
59 - Delete two instances:
60 $ acloud.par delete --instance_names
Fang Dengfed6a6f2017-03-01 18:27:28 -080061 ins-b638cdba-3744001-gce-x86-phone-userdebug-fastbuild3c-linux
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070062 --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"
Tri Vo8e292532016-10-01 16:55:51 -070078LOGGER_NAME = "acloud_main"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070079
80# Commands
81CMD_CREATE = "create"
82CMD_DELETE = "delete"
83CMD_CLEANUP = "cleanup"
Fang Deng69498c32017-03-02 14:29:30 -080084CMD_SSHKEY = "project_sshkey"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070085
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
Fang Deng69498c32017-03-02 14:29:30 -0800199 # Command "project_sshkey"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700200 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,
Fang Deng69498c32017-03-02 14:29:30 -0800215 help="Absolute path to the file that contains the public rsa key "
216 "that will be added as project-wide ssh key.")
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700217 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
Fang Dengcef4b112017-03-02 11:20:17 -0800326 # Check access.
327 device_driver.CheckAccess(cfg)
328
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700329 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
Tri Vo8e292532016-10-01 16:55:51 -0700357
358
359if __name__ == "__main__":
360 main(sys.argv[1:])