blob: a5a8518759bdc7492139f52723204fe214121a53 [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
29from acloud.public import avd
30from acloud.public import errors
31from 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
cylan31fc5332018-09-17 22:12:08 +0800134 @utils.TimeExecute(function_description="Waiting for AVD(s) to boot up")
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700135 def WaitForBoot(self):
136 """Waits for all devices to boot up.
137
138 Returns:
139 A dictionary that contains all the failures.
140 The key is the name of the instance that fails to boot,
141 and the value is an errors.DeviceBootError object.
142 """
143 failures = {}
144 for device in self._devices:
145 try:
146 self._compute_client.WaitForBoot(device.instance_name)
147 except errors.DeviceBootError as e:
148 failures[device.instance_name] = e
149 return failures
150
Kevin Chengce6cfb02018-12-04 13:21:31 -0800151 def PullLogs(self, source_files, output_dir, user=None, ssh_rsa_path=None):
152 """Tar logs from GCE instance into output_dir.
153
154 Args:
155 source_files: List of file names to be pulled.
156 output_dir: String. The output file dirtory
157 user: String, the ssh username to access GCE
158 ssh_rsa_path: String, the ssh rsa key path to access GCE
159
160 Returns:
161 The file dictionary with file_path and file_name
162 """
163
164 file_dict = {}
165 for device in self._devices:
166 if isinstance(source_files, basestring):
167 source_files = [source_files]
168 for source_file in source_files:
169 file_name = "%s_%s" % (device.instance_name,
170 os.path.basename(source_file))
171 dst_file = os.path.join(output_dir, file_name)
172 logger.info("Pull %s for instance %s with user %s to %s",
173 source_file, device.instance_name, user, dst_file)
174 try:
175 utils.ScpPullFile(source_file, dst_file, device.ip,
176 user_name=user, rsa_key_file=ssh_rsa_path)
177 file_dict[dst_file] = file_name
178 except errors.DeviceConnectionError as e:
179 logger.warning("Failed to pull %s from instance %s: %s",
180 source_file, device.instance_name, e)
181 return file_dict
182
183 def CollectSerialPortLogs(self, output_file,
184 port=constants.DEFAULT_SERIAL_PORT):
185 """Tar the instance serial logs into specified output_file.
186
187 Args:
188 output_file: String, the output tar file path
189 port: The serial port number to be collected
190 """
191 # For emulator, the serial log is the virtual host serial log.
192 # For GCE AVD device, the serial log is the AVD device serial log.
193 with utils.TempDir() as tempdir:
194 src_dict = {}
195 for device in self._devices:
196 logger.info("Store instance %s serial port %s output to %s",
197 device.instance_name, port, output_file)
198 serial_log = self._compute_client.GetSerialPortOutput(
199 instance=device.instance_name, port=port)
200 file_name = "%s_serial_%s.log" % (device.instance_name, port)
201 file_path = os.path.join(tempdir, file_name)
202 src_dict[file_path] = file_name
203 with open(file_path, "w") as f:
204 f.write(serial_log.encode("utf-8"))
205 utils.MakeTarFile(src_dict, output_file)
206
207 def CollectLogcats(self, output_file, ssh_user, ssh_rsa_path):
208 """Tar the instances' logcat and other logs into specified output_file.
209
210 Args:
211 output_file: String, the output tar file path
212 ssh_user: The ssh user name
213 ssh_rsa_path: The ssh rsa key path
214 """
215 with utils.TempDir() as tempdir:
216 file_dict = {}
217 if getattr(self._device_factory, "LOG_FILES", None):
218 file_dict = self.PullLogs(
219 self._device_factory.LOG_FILES, tempdir, user=ssh_user,
220 ssh_rsa_path=ssh_rsa_path)
221 # If the device is auto-connected, get adb logcat
222 for file_path, file_name in self._CollectAdbLogcats(
223 tempdir).items():
224 file_dict[file_path] = file_name
225 utils.MakeTarFile(file_dict, output_file)
226
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700227 @property
228 def devices(self):
229 """Returns a list of devices in the pool.
230
231 Returns:
232 A list of devices in the pool.
233 """
234 return self._devices
Kevin Chengb5963882018-05-09 00:06:27 -0700235
Kevin Chengce6cfb02018-12-04 13:21:31 -0800236# TODO: Delete unused-argument when b/119614469 is resolved.
237# pylint: disable=unused-argument
cylan66713722018-10-06 01:38:26 +0800238# pylint: disable=too-many-locals
239def CreateDevices(command, cfg, device_factory, num, report_internal_ip=False,
Kevin Chengce6cfb02018-12-04 13:21:31 -0800240 autoconnect=False, serial_log_file=None, logcat_file=None):
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700241 """Create a set of devices using the given factory.
Kevin Chengb5963882018-05-09 00:06:27 -0700242
herbertxuedf01c422018-09-06 19:52:52 +0800243 Main jobs in create devices.
244 1. Create GCE instance: Launch instance in GCP(Google Cloud Platform).
245 2. Starting up AVD: Wait device boot up.
246
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700247 Args:
248 command: The name of the command, used for reporting.
249 cfg: An AcloudConfig instance.
250 device_factory: A factory capable of producing a single device.
251 num: The number of devices to create.
Kevin Cheng86d43c72018-08-30 10:59:14 -0700252 report_internal_ip: Boolean to report the internal ip instead of
253 external ip.
Kevin Chengce6cfb02018-12-04 13:21:31 -0800254 serial_log_file: String, the file path to tar the serial logs.
255 logcat_file: String, the file path to tar the logcats.
256 autoconnect: Boolean, whether to auto connect to device.
Kevin Chengb5963882018-05-09 00:06:27 -0700257
herbertxuedf01c422018-09-06 19:52:52 +0800258 Raises:
259 errors: Create instance fail.
260
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700261 Returns:
262 A Report instance.
263 """
264 reporter = report.Report(command=command)
265 try:
266 CreateSshKeyPairIfNecessary(cfg)
267 device_pool = DevicePool(device_factory)
cylan31fc5332018-09-17 22:12:08 +0800268 device_pool.CreateDevices(num)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700269 failures = device_pool.WaitForBoot()
herbertxuedf01c422018-09-06 19:52:52 +0800270 if failures:
271 reporter.SetStatus(report.Status.BOOT_FAIL)
herbertxuedf01c422018-09-06 19:52:52 +0800272 else:
273 reporter.SetStatus(report.Status.SUCCESS)
Kevin Chengce6cfb02018-12-04 13:21:31 -0800274
275 # Collect logs
276 if serial_log_file:
277 device_pool.CollectSerialPortLogs(
278 serial_log_file, port=constants.DEFAULT_SERIAL_PORT)
279 # TODO(b/119614469): Refactor CollectLogcats into a utils lib and
280 # turn it on inside the reporting loop.
281 # if logcat_file:
282 # device_pool.CollectLogcats(logcat_file, ssh_user, ssh_rsa_path)
283
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700284 # Write result to report.
285 for device in device_pool.devices:
cylan66713722018-10-06 01:38:26 +0800286 ip = (device.ip.internal if report_internal_ip
287 else device.ip.external)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700288 device_dict = {
cylan66713722018-10-06 01:38:26 +0800289 "ip": ip,
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700290 "instance_name": device.instance_name
291 }
cylan66713722018-10-06 01:38:26 +0800292 if autoconnect:
293 forwarded_ports = utils.AutoConnect(ip,
294 cfg.ssh_private_key_path,
cylan4569dca2018-11-02 12:12:53 +0800295 constants.CF_TARGET_VNC_PORT,
296 constants.CF_TARGET_ADB_PORT,
cylan66713722018-10-06 01:38:26 +0800297 getpass.getuser())
298 device_dict[constants.VNC_PORT] = forwarded_ports.vnc_port
299 device_dict[constants.ADB_PORT] = forwarded_ports.adb_port
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700300 if device.instance_name in failures:
301 reporter.AddData(key="devices_failing_boot", value=device_dict)
302 reporter.AddError(str(failures[device.instance_name]))
303 else:
304 reporter.AddData(key="devices", value=device_dict)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700305 except errors.DriverError as e:
306 reporter.AddError(str(e))
307 reporter.SetStatus(report.Status.FAIL)
308 return reporter