blob: 21181a1e9eb785ead565a8471ca721ae7e089207 [file] [log] [blame]
Kevin Chengb5963882018-05-09 00:06:27 -07001#!/usr/bin/env python
2#
3# Copyright 2018 - 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.
Kevin Chengb5963882018-05-09 00:06:27 -070016"""Common operations between managing GCE and Cuttlefish devices.
17
18This module provides the common operations between managing GCE (device_driver)
19and Cuttlefish (create_cuttlefish_action) devices. Should not be called
20directly.
21"""
22
herbertxuedf01c422018-09-06 19:52:52 +080023from __future__ import print_function
cylan66713722018-10-06 01:38:26 +080024import getpass
Kevin Chengb5963882018-05-09 00:06:27 -070025import logging
Kevin Chengce6cfb02018-12-04 13:21:31 -080026import os
27import subprocess
Kevin Chengb5963882018-05-09 00:06:27 -070028
Sam Chiu7de3b232018-12-06 19:45:52 +080029from acloud import errors
Kevin Chengb5963882018-05-09 00:06:27 -070030from acloud.public import avd
Kevin Chengb5963882018-05-09 00:06:27 -070031from acloud.public import report
cylan66713722018-10-06 01:38:26 +080032from acloud.internal import constants
Kevin Chengb5963882018-05-09 00:06:27 -070033from acloud.internal.lib import utils
34
35logger = logging.getLogger(__name__)
36
Kevin Chengb5963882018-05-09 00:06:27 -070037def CreateSshKeyPairIfNecessary(cfg):
Kevin Cheng3031f8a2018-05-16 13:21:51 -070038 """Create ssh key pair if necessary.
Kevin Chengb5963882018-05-09 00:06:27 -070039
Kevin Cheng3031f8a2018-05-16 13:21:51 -070040 Args:
41 cfg: An Acloudconfig instance.
Kevin Chengb5963882018-05-09 00:06:27 -070042
Kevin Cheng3031f8a2018-05-16 13:21:51 -070043 Raises:
44 error.DriverError: If it falls into an unexpected condition.
45 """
46 if not cfg.ssh_public_key_path:
47 logger.warning(
48 "ssh_public_key_path is not specified in acloud config. "
49 "Project-wide public key will "
50 "be used when creating AVD instances. "
51 "Please ensure you have the correct private half of "
52 "a project-wide public key if you want to ssh into the "
53 "instances after creation.")
54 elif cfg.ssh_public_key_path and not cfg.ssh_private_key_path:
55 logger.warning(
56 "Only ssh_public_key_path is specified in acloud config, "
57 "but ssh_private_key_path is missing. "
58 "Please ensure you have the correct private half "
59 "if you want to ssh into the instances after creation.")
60 elif cfg.ssh_public_key_path and cfg.ssh_private_key_path:
61 utils.CreateSshKeyPairIfNotExist(cfg.ssh_private_key_path,
62 cfg.ssh_public_key_path)
63 else:
64 # Should never reach here.
65 raise errors.DriverError(
66 "Unexpected error in CreateSshKeyPairIfNecessary")
Kevin Chengb5963882018-05-09 00:06:27 -070067
68
69class DevicePool(object):
Kevin Cheng3031f8a2018-05-16 13:21:51 -070070 """A class that manages a pool of virtual devices.
Kevin Chengb5963882018-05-09 00:06:27 -070071
Kevin Cheng3031f8a2018-05-16 13:21:51 -070072 Attributes:
73 devices: A list of devices in the pool.
Kevin Chengb5963882018-05-09 00:06:27 -070074 """
75
Kevin Cheng3031f8a2018-05-16 13:21:51 -070076 def __init__(self, device_factory, devices=None):
77 """Constructs a new DevicePool.
Kevin Chengb5963882018-05-09 00:06:27 -070078
Kevin Cheng3031f8a2018-05-16 13:21:51 -070079 Args:
80 device_factory: A device factory capable of producing a goldfish or
81 cuttlefish device. The device factory must expose an attribute with
82 the credentials that can be used to retrieve information from the
83 constructed device.
84 devices: List of devices managed by this pool.
85 """
86 self._devices = devices or []
87 self._device_factory = device_factory
88 self._compute_client = device_factory.GetComputeClient()
Kevin Chengb5963882018-05-09 00:06:27 -070089
Kevin Chengce6cfb02018-12-04 13:21:31 -080090 def _CollectAdbLogcats(self, output_dir):
91 """Collect Adb logcats.
92
93 Args:
94 output_dir: String, the output file directory to store adb logcats.
95
96 Returns:
97 The file information dictionary with file path and file name.
98 """
99 file_dict = {}
100 for device in self._devices:
101 if not device.adb_port:
102 # If device adb tunnel is not established, do not do adb logcat
103 continue
104 file_name = "%s_adb_logcat.log" % device.instance_name
105 full_file_path = os.path.join(output_dir, file_name)
106 logger.info("Get adb %s:%s logcat for instance %s",
107 constants.LOCALHOST, device.adb_port,
108 device.instance_name)
109 try:
110 subprocess.check_call(
111 ["adb -s %s:%s logcat -b all -d > %s" % (
112 constants.LOCALHOST, device.adb_port, full_file_path)],
113 shell=True)
114 file_dict[full_file_path] = file_name
115 except subprocess.CalledProcessError:
116 logging.error("Failed to get adb logcat for %s for instance %s",
117 device.serial_number, device.instance_name)
118 return file_dict
119
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700120 def CreateDevices(self, num):
121 """Creates |num| devices for given build_target and build_id.
Kevin Chengb5963882018-05-09 00:06:27 -0700122
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700123 Args:
124 num: Number of devices to create.
125 """
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700126 # Create host instances for cuttlefish/goldfish device.
127 # Currently one instance supports only 1 device.
128 for _ in range(num):
129 instance = self._device_factory.CreateInstance()
130 ip = self._compute_client.GetInstanceIP(instance)
131 self.devices.append(
132 avd.AndroidVirtualDevice(ip=ip, instance_name=instance))
133
Kevin Cheng4bfd69e2019-01-11 16:32:56 -0800134 @utils.TimeExecute(function_description="Waiting for AVD(s) to boot up",
135 result_evaluator=utils.BootEvaluator)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700136 def WaitForBoot(self):
137 """Waits for all devices to boot up.
138
139 Returns:
140 A dictionary that contains all the failures.
141 The key is the name of the instance that fails to boot,
142 and the value is an errors.DeviceBootError object.
143 """
144 failures = {}
145 for device in self._devices:
146 try:
147 self._compute_client.WaitForBoot(device.instance_name)
148 except errors.DeviceBootError as e:
149 failures[device.instance_name] = e
150 return failures
151
Kevin Chengce6cfb02018-12-04 13:21:31 -0800152 def PullLogs(self, source_files, output_dir, user=None, ssh_rsa_path=None):
153 """Tar logs from GCE instance into output_dir.
154
155 Args:
156 source_files: List of file names to be pulled.
157 output_dir: String. The output file dirtory
158 user: String, the ssh username to access GCE
159 ssh_rsa_path: String, the ssh rsa key path to access GCE
160
161 Returns:
162 The file dictionary with file_path and file_name
163 """
164
165 file_dict = {}
166 for device in self._devices:
167 if isinstance(source_files, basestring):
168 source_files = [source_files]
169 for source_file in source_files:
170 file_name = "%s_%s" % (device.instance_name,
171 os.path.basename(source_file))
172 dst_file = os.path.join(output_dir, file_name)
173 logger.info("Pull %s for instance %s with user %s to %s",
174 source_file, device.instance_name, user, dst_file)
175 try:
176 utils.ScpPullFile(source_file, dst_file, device.ip,
177 user_name=user, rsa_key_file=ssh_rsa_path)
178 file_dict[dst_file] = file_name
179 except errors.DeviceConnectionError as e:
180 logger.warning("Failed to pull %s from instance %s: %s",
181 source_file, device.instance_name, e)
182 return file_dict
183
184 def CollectSerialPortLogs(self, output_file,
185 port=constants.DEFAULT_SERIAL_PORT):
186 """Tar the instance serial logs into specified output_file.
187
188 Args:
189 output_file: String, the output tar file path
190 port: The serial port number to be collected
191 """
192 # For emulator, the serial log is the virtual host serial log.
193 # For GCE AVD device, the serial log is the AVD device serial log.
194 with utils.TempDir() as tempdir:
195 src_dict = {}
196 for device in self._devices:
197 logger.info("Store instance %s serial port %s output to %s",
198 device.instance_name, port, output_file)
199 serial_log = self._compute_client.GetSerialPortOutput(
200 instance=device.instance_name, port=port)
201 file_name = "%s_serial_%s.log" % (device.instance_name, port)
202 file_path = os.path.join(tempdir, file_name)
203 src_dict[file_path] = file_name
204 with open(file_path, "w") as f:
205 f.write(serial_log.encode("utf-8"))
206 utils.MakeTarFile(src_dict, output_file)
207
208 def CollectLogcats(self, output_file, ssh_user, ssh_rsa_path):
209 """Tar the instances' logcat and other logs into specified output_file.
210
211 Args:
212 output_file: String, the output tar file path
213 ssh_user: The ssh user name
214 ssh_rsa_path: The ssh rsa key path
215 """
216 with utils.TempDir() as tempdir:
217 file_dict = {}
218 if getattr(self._device_factory, "LOG_FILES", None):
219 file_dict = self.PullLogs(
220 self._device_factory.LOG_FILES, tempdir, user=ssh_user,
221 ssh_rsa_path=ssh_rsa_path)
222 # If the device is auto-connected, get adb logcat
223 for file_path, file_name in self._CollectAdbLogcats(
224 tempdir).items():
225 file_dict[file_path] = file_name
226 utils.MakeTarFile(file_dict, output_file)
227
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700228 @property
229 def devices(self):
230 """Returns a list of devices in the pool.
231
232 Returns:
233 A list of devices in the pool.
234 """
235 return self._devices
Kevin Chengb5963882018-05-09 00:06:27 -0700236
Kevin Chengce6cfb02018-12-04 13:21:31 -0800237# TODO: Delete unused-argument when b/119614469 is resolved.
238# pylint: disable=unused-argument
cylan66713722018-10-06 01:38:26 +0800239# pylint: disable=too-many-locals
240def CreateDevices(command, cfg, device_factory, num, report_internal_ip=False,
Kevin Chengce6cfb02018-12-04 13:21:31 -0800241 autoconnect=False, serial_log_file=None, logcat_file=None):
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700242 """Create a set of devices using the given factory.
Kevin Chengb5963882018-05-09 00:06:27 -0700243
herbertxuedf01c422018-09-06 19:52:52 +0800244 Main jobs in create devices.
245 1. Create GCE instance: Launch instance in GCP(Google Cloud Platform).
246 2. Starting up AVD: Wait device boot up.
247
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700248 Args:
249 command: The name of the command, used for reporting.
250 cfg: An AcloudConfig instance.
251 device_factory: A factory capable of producing a single device.
252 num: The number of devices to create.
Kevin Cheng86d43c72018-08-30 10:59:14 -0700253 report_internal_ip: Boolean to report the internal ip instead of
254 external ip.
Kevin Chengce6cfb02018-12-04 13:21:31 -0800255 serial_log_file: String, the file path to tar the serial logs.
256 logcat_file: String, the file path to tar the logcats.
257 autoconnect: Boolean, whether to auto connect to device.
Kevin Chengb5963882018-05-09 00:06:27 -0700258
herbertxuedf01c422018-09-06 19:52:52 +0800259 Raises:
260 errors: Create instance fail.
261
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700262 Returns:
263 A Report instance.
264 """
265 reporter = report.Report(command=command)
266 try:
267 CreateSshKeyPairIfNecessary(cfg)
268 device_pool = DevicePool(device_factory)
cylan31fc5332018-09-17 22:12:08 +0800269 device_pool.CreateDevices(num)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700270 failures = device_pool.WaitForBoot()
herbertxuedf01c422018-09-06 19:52:52 +0800271 if failures:
272 reporter.SetStatus(report.Status.BOOT_FAIL)
herbertxuedf01c422018-09-06 19:52:52 +0800273 else:
274 reporter.SetStatus(report.Status.SUCCESS)
Kevin Chengce6cfb02018-12-04 13:21:31 -0800275
276 # Collect logs
277 if serial_log_file:
278 device_pool.CollectSerialPortLogs(
279 serial_log_file, port=constants.DEFAULT_SERIAL_PORT)
280 # TODO(b/119614469): Refactor CollectLogcats into a utils lib and
281 # turn it on inside the reporting loop.
282 # if logcat_file:
283 # device_pool.CollectLogcats(logcat_file, ssh_user, ssh_rsa_path)
284
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700285 # Write result to report.
286 for device in device_pool.devices:
cylan66713722018-10-06 01:38:26 +0800287 ip = (device.ip.internal if report_internal_ip
288 else device.ip.external)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700289 device_dict = {
cylan66713722018-10-06 01:38:26 +0800290 "ip": ip,
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700291 "instance_name": device.instance_name
292 }
cylan66713722018-10-06 01:38:26 +0800293 if autoconnect:
294 forwarded_ports = utils.AutoConnect(ip,
295 cfg.ssh_private_key_path,
cylan4569dca2018-11-02 12:12:53 +0800296 constants.CF_TARGET_VNC_PORT,
297 constants.CF_TARGET_ADB_PORT,
cylan66713722018-10-06 01:38:26 +0800298 getpass.getuser())
299 device_dict[constants.VNC_PORT] = forwarded_ports.vnc_port
300 device_dict[constants.ADB_PORT] = forwarded_ports.adb_port
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700301 if device.instance_name in failures:
302 reporter.AddData(key="devices_failing_boot", value=device_dict)
303 reporter.AddError(str(failures[device.instance_name]))
304 else:
305 reporter.AddData(key="devices", value=device_dict)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700306 except errors.DriverError as e:
307 reporter.AddError(str(e))
308 reporter.SetStatus(report.Status.FAIL)
309 return reporter