blob: b7df5ddac5597932bdb8e71d871a08448a9a6af3 [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
cylan66713722018-10-06 01:38:26 +080037#For the cuttlefish remote instances: adb port is 6520 and vnc is 6444.
38_CF_TARGET_ADB_PORT = 6520
39_CF_TARGET_VNC_PORT = 6444
Kevin Chengb5963882018-05-09 00:06:27 -070040
41def CreateSshKeyPairIfNecessary(cfg):
Kevin Cheng3031f8a2018-05-16 13:21:51 -070042 """Create ssh key pair if necessary.
Kevin Chengb5963882018-05-09 00:06:27 -070043
Kevin Cheng3031f8a2018-05-16 13:21:51 -070044 Args:
45 cfg: An Acloudconfig instance.
Kevin Chengb5963882018-05-09 00:06:27 -070046
Kevin Cheng3031f8a2018-05-16 13:21:51 -070047 Raises:
48 error.DriverError: If it falls into an unexpected condition.
49 """
50 if not cfg.ssh_public_key_path:
51 logger.warning(
52 "ssh_public_key_path is not specified in acloud config. "
53 "Project-wide public key will "
54 "be used when creating AVD instances. "
55 "Please ensure you have the correct private half of "
56 "a project-wide public key if you want to ssh into the "
57 "instances after creation.")
58 elif cfg.ssh_public_key_path and not cfg.ssh_private_key_path:
59 logger.warning(
60 "Only ssh_public_key_path is specified in acloud config, "
61 "but ssh_private_key_path is missing. "
62 "Please ensure you have the correct private half "
63 "if you want to ssh into the instances after creation.")
64 elif cfg.ssh_public_key_path and cfg.ssh_private_key_path:
65 utils.CreateSshKeyPairIfNotExist(cfg.ssh_private_key_path,
66 cfg.ssh_public_key_path)
67 else:
68 # Should never reach here.
69 raise errors.DriverError(
70 "Unexpected error in CreateSshKeyPairIfNecessary")
Kevin Chengb5963882018-05-09 00:06:27 -070071
72
73class DevicePool(object):
Kevin Cheng3031f8a2018-05-16 13:21:51 -070074 """A class that manages a pool of virtual devices.
Kevin Chengb5963882018-05-09 00:06:27 -070075
Kevin Cheng3031f8a2018-05-16 13:21:51 -070076 Attributes:
77 devices: A list of devices in the pool.
Kevin Chengb5963882018-05-09 00:06:27 -070078 """
79
Kevin Cheng3031f8a2018-05-16 13:21:51 -070080 def __init__(self, device_factory, devices=None):
81 """Constructs a new DevicePool.
Kevin Chengb5963882018-05-09 00:06:27 -070082
Kevin Cheng3031f8a2018-05-16 13:21:51 -070083 Args:
84 device_factory: A device factory capable of producing a goldfish or
85 cuttlefish device. The device factory must expose an attribute with
86 the credentials that can be used to retrieve information from the
87 constructed device.
88 devices: List of devices managed by this pool.
89 """
90 self._devices = devices or []
91 self._device_factory = device_factory
92 self._compute_client = device_factory.GetComputeClient()
Kevin Chengb5963882018-05-09 00:06:27 -070093
Kevin Chengce6cfb02018-12-04 13:21:31 -080094 def _CollectAdbLogcats(self, output_dir):
95 """Collect Adb logcats.
96
97 Args:
98 output_dir: String, the output file directory to store adb logcats.
99
100 Returns:
101 The file information dictionary with file path and file name.
102 """
103 file_dict = {}
104 for device in self._devices:
105 if not device.adb_port:
106 # If device adb tunnel is not established, do not do adb logcat
107 continue
108 file_name = "%s_adb_logcat.log" % device.instance_name
109 full_file_path = os.path.join(output_dir, file_name)
110 logger.info("Get adb %s:%s logcat for instance %s",
111 constants.LOCALHOST, device.adb_port,
112 device.instance_name)
113 try:
114 subprocess.check_call(
115 ["adb -s %s:%s logcat -b all -d > %s" % (
116 constants.LOCALHOST, device.adb_port, full_file_path)],
117 shell=True)
118 file_dict[full_file_path] = file_name
119 except subprocess.CalledProcessError:
120 logging.error("Failed to get adb logcat for %s for instance %s",
121 device.serial_number, device.instance_name)
122 return file_dict
123
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700124 def CreateDevices(self, num):
125 """Creates |num| devices for given build_target and build_id.
Kevin Chengb5963882018-05-09 00:06:27 -0700126
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700127 Args:
128 num: Number of devices to create.
129 """
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700130 # Create host instances for cuttlefish/goldfish device.
131 # Currently one instance supports only 1 device.
132 for _ in range(num):
133 instance = self._device_factory.CreateInstance()
134 ip = self._compute_client.GetInstanceIP(instance)
135 self.devices.append(
136 avd.AndroidVirtualDevice(ip=ip, instance_name=instance))
137
cylan31fc5332018-09-17 22:12:08 +0800138 @utils.TimeExecute(function_description="Waiting for AVD(s) to boot up")
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700139 def WaitForBoot(self):
140 """Waits for all devices to boot up.
141
142 Returns:
143 A dictionary that contains all the failures.
144 The key is the name of the instance that fails to boot,
145 and the value is an errors.DeviceBootError object.
146 """
147 failures = {}
148 for device in self._devices:
149 try:
150 self._compute_client.WaitForBoot(device.instance_name)
151 except errors.DeviceBootError as e:
152 failures[device.instance_name] = e
153 return failures
154
Kevin Chengce6cfb02018-12-04 13:21:31 -0800155 def PullLogs(self, source_files, output_dir, user=None, ssh_rsa_path=None):
156 """Tar logs from GCE instance into output_dir.
157
158 Args:
159 source_files: List of file names to be pulled.
160 output_dir: String. The output file dirtory
161 user: String, the ssh username to access GCE
162 ssh_rsa_path: String, the ssh rsa key path to access GCE
163
164 Returns:
165 The file dictionary with file_path and file_name
166 """
167
168 file_dict = {}
169 for device in self._devices:
170 if isinstance(source_files, basestring):
171 source_files = [source_files]
172 for source_file in source_files:
173 file_name = "%s_%s" % (device.instance_name,
174 os.path.basename(source_file))
175 dst_file = os.path.join(output_dir, file_name)
176 logger.info("Pull %s for instance %s with user %s to %s",
177 source_file, device.instance_name, user, dst_file)
178 try:
179 utils.ScpPullFile(source_file, dst_file, device.ip,
180 user_name=user, rsa_key_file=ssh_rsa_path)
181 file_dict[dst_file] = file_name
182 except errors.DeviceConnectionError as e:
183 logger.warning("Failed to pull %s from instance %s: %s",
184 source_file, device.instance_name, e)
185 return file_dict
186
187 def CollectSerialPortLogs(self, output_file,
188 port=constants.DEFAULT_SERIAL_PORT):
189 """Tar the instance serial logs into specified output_file.
190
191 Args:
192 output_file: String, the output tar file path
193 port: The serial port number to be collected
194 """
195 # For emulator, the serial log is the virtual host serial log.
196 # For GCE AVD device, the serial log is the AVD device serial log.
197 with utils.TempDir() as tempdir:
198 src_dict = {}
199 for device in self._devices:
200 logger.info("Store instance %s serial port %s output to %s",
201 device.instance_name, port, output_file)
202 serial_log = self._compute_client.GetSerialPortOutput(
203 instance=device.instance_name, port=port)
204 file_name = "%s_serial_%s.log" % (device.instance_name, port)
205 file_path = os.path.join(tempdir, file_name)
206 src_dict[file_path] = file_name
207 with open(file_path, "w") as f:
208 f.write(serial_log.encode("utf-8"))
209 utils.MakeTarFile(src_dict, output_file)
210
211 def CollectLogcats(self, output_file, ssh_user, ssh_rsa_path):
212 """Tar the instances' logcat and other logs into specified output_file.
213
214 Args:
215 output_file: String, the output tar file path
216 ssh_user: The ssh user name
217 ssh_rsa_path: The ssh rsa key path
218 """
219 with utils.TempDir() as tempdir:
220 file_dict = {}
221 if getattr(self._device_factory, "LOG_FILES", None):
222 file_dict = self.PullLogs(
223 self._device_factory.LOG_FILES, tempdir, user=ssh_user,
224 ssh_rsa_path=ssh_rsa_path)
225 # If the device is auto-connected, get adb logcat
226 for file_path, file_name in self._CollectAdbLogcats(
227 tempdir).items():
228 file_dict[file_path] = file_name
229 utils.MakeTarFile(file_dict, output_file)
230
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700231 @property
232 def devices(self):
233 """Returns a list of devices in the pool.
234
235 Returns:
236 A list of devices in the pool.
237 """
238 return self._devices
Kevin Chengb5963882018-05-09 00:06:27 -0700239
Kevin Chengce6cfb02018-12-04 13:21:31 -0800240# TODO: Delete unused-argument when b/119614469 is resolved.
241# pylint: disable=unused-argument
cylan66713722018-10-06 01:38:26 +0800242# pylint: disable=too-many-locals
243def CreateDevices(command, cfg, device_factory, num, report_internal_ip=False,
Kevin Chengce6cfb02018-12-04 13:21:31 -0800244 autoconnect=False, serial_log_file=None, logcat_file=None):
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700245 """Create a set of devices using the given factory.
Kevin Chengb5963882018-05-09 00:06:27 -0700246
herbertxuedf01c422018-09-06 19:52:52 +0800247 Main jobs in create devices.
248 1. Create GCE instance: Launch instance in GCP(Google Cloud Platform).
249 2. Starting up AVD: Wait device boot up.
250
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700251 Args:
252 command: The name of the command, used for reporting.
253 cfg: An AcloudConfig instance.
254 device_factory: A factory capable of producing a single device.
255 num: The number of devices to create.
Kevin Cheng86d43c72018-08-30 10:59:14 -0700256 report_internal_ip: Boolean to report the internal ip instead of
257 external ip.
Kevin Chengce6cfb02018-12-04 13:21:31 -0800258 serial_log_file: String, the file path to tar the serial logs.
259 logcat_file: String, the file path to tar the logcats.
260 autoconnect: Boolean, whether to auto connect to device.
Kevin Chengb5963882018-05-09 00:06:27 -0700261
herbertxuedf01c422018-09-06 19:52:52 +0800262 Raises:
263 errors: Create instance fail.
264
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700265 Returns:
266 A Report instance.
267 """
268 reporter = report.Report(command=command)
269 try:
270 CreateSshKeyPairIfNecessary(cfg)
271 device_pool = DevicePool(device_factory)
cylan31fc5332018-09-17 22:12:08 +0800272 device_pool.CreateDevices(num)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700273 failures = device_pool.WaitForBoot()
herbertxuedf01c422018-09-06 19:52:52 +0800274 if failures:
275 reporter.SetStatus(report.Status.BOOT_FAIL)
herbertxuedf01c422018-09-06 19:52:52 +0800276 else:
277 reporter.SetStatus(report.Status.SUCCESS)
Kevin Chengce6cfb02018-12-04 13:21:31 -0800278
279 # Collect logs
280 if serial_log_file:
281 device_pool.CollectSerialPortLogs(
282 serial_log_file, port=constants.DEFAULT_SERIAL_PORT)
283 # TODO(b/119614469): Refactor CollectLogcats into a utils lib and
284 # turn it on inside the reporting loop.
285 # if logcat_file:
286 # device_pool.CollectLogcats(logcat_file, ssh_user, ssh_rsa_path)
287
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700288 # Write result to report.
289 for device in device_pool.devices:
cylan66713722018-10-06 01:38:26 +0800290 ip = (device.ip.internal if report_internal_ip
291 else device.ip.external)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700292 device_dict = {
cylan66713722018-10-06 01:38:26 +0800293 "ip": ip,
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700294 "instance_name": device.instance_name
295 }
cylan66713722018-10-06 01:38:26 +0800296 if autoconnect:
297 forwarded_ports = utils.AutoConnect(ip,
298 cfg.ssh_private_key_path,
299 _CF_TARGET_VNC_PORT,
300 _CF_TARGET_ADB_PORT,
301 getpass.getuser())
302 device_dict[constants.VNC_PORT] = forwarded_ports.vnc_port
303 device_dict[constants.ADB_PORT] = forwarded_ports.adb_port
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700304 if device.instance_name in failures:
305 reporter.AddData(key="devices_failing_boot", value=device_dict)
306 reporter.AddError(str(failures[device.instance_name]))
307 else:
308 reporter.AddData(key="devices", value=device_dict)
Kevin Cheng3031f8a2018-05-16 13:21:51 -0700309 except errors.DriverError as e:
310 reporter.AddError(str(e))
311 reporter.SetStatus(report.Status.FAIL)
312 return reporter