1#!/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. 16"""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 23import logging 24import os 25 26from acloud import errors 27from acloud.public import avd 28from acloud.public import report 29from acloud.internal import constants 30from acloud.internal.lib import utils 31from acloud.internal.lib.adb_tools import AdbTools 32 33 34logger = logging.getLogger(__name__) 35 36 37def CreateSshKeyPairIfNecessary(cfg): 38 """Create ssh key pair if necessary. 39 40 Args: 41 cfg: An Acloudconfig instance. 42 43 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") 67 68 69class DevicePool(object): 70 """A class that manages a pool of virtual devices. 71 72 Attributes: 73 devices: A list of devices in the pool. 74 """ 75 76 def __init__(self, device_factory, devices=None): 77 """Constructs a new DevicePool. 78 79 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() 89 90 def CreateDevices(self, num): 91 """Creates |num| devices for given build_target and build_id. 92 93 Args: 94 num: Number of devices to create. 95 """ 96 # Create host instances for cuttlefish/goldfish device. 97 # Currently one instance supports only 1 device. 98 for _ in range(num): 99 instance = self._device_factory.CreateInstance() 100 ip = self._compute_client.GetInstanceIP(instance) 101 time_info = self._compute_client.execution_time if hasattr( 102 self._compute_client, "execution_time") else {} 103 self.devices.append( 104 avd.AndroidVirtualDevice(ip=ip, instance_name=instance, 105 time_info=time_info)) 106 107 @utils.TimeExecute(function_description="Waiting for AVD(s) to boot up", 108 result_evaluator=utils.BootEvaluator) 109 def WaitForBoot(self, boot_timeout_secs): 110 """Waits for all devices to boot up. 111 112 Args: 113 boot_timeout_secs: Integer, the maximum time in seconds used to 114 wait for the AVD to boot. 115 116 Returns: 117 A dictionary that contains all the failures. 118 The key is the name of the instance that fails to boot, 119 and the value is an errors.DeviceBootError object. 120 """ 121 failures = {} 122 for device in self._devices: 123 try: 124 self._compute_client.WaitForBoot(device.instance_name, boot_timeout_secs) 125 except errors.DeviceBootError as e: 126 failures[device.instance_name] = e 127 return failures 128 129 def CollectSerialPortLogs(self, output_file, 130 port=constants.DEFAULT_SERIAL_PORT): 131 """Tar the instance serial logs into specified output_file. 132 133 Args: 134 output_file: String, the output tar file path 135 port: The serial port number to be collected 136 """ 137 # For emulator, the serial log is the virtual host serial log. 138 # For GCE AVD device, the serial log is the AVD device serial log. 139 with utils.TempDir() as tempdir: 140 src_dict = {} 141 for device in self._devices: 142 logger.info("Store instance %s serial port %s output to %s", 143 device.instance_name, port, output_file) 144 serial_log = self._compute_client.GetSerialPortOutput( 145 instance=device.instance_name, port=port) 146 file_name = "%s_serial_%s.log" % (device.instance_name, port) 147 file_path = os.path.join(tempdir, file_name) 148 src_dict[file_path] = file_name 149 with open(file_path, "w") as f: 150 f.write(serial_log.encode("utf-8")) 151 utils.MakeTarFile(src_dict, output_file) 152 153 def SetDeviceBuildInfo(self): 154 """Add devices build info.""" 155 for device in self._devices: 156 device.build_info = self._device_factory.GetBuildInfoDict() 157 158 @property 159 def devices(self): 160 """Returns a list of devices in the pool. 161 162 Returns: 163 A list of devices in the pool. 164 """ 165 return self._devices 166 167# pylint: disable=too-many-locals,unused-argument,too-many-branches 168def CreateDevices(command, cfg, device_factory, num, avd_type, 169 report_internal_ip=False, autoconnect=False, 170 serial_log_file=None, client_adb_port=None, 171 boot_timeout_secs=None, unlock_screen=False, 172 wait_for_boot=True): 173 """Create a set of devices using the given factory. 174 175 Main jobs in create devices. 176 1. Create GCE instance: Launch instance in GCP(Google Cloud Platform). 177 2. Starting up AVD: Wait device boot up. 178 179 Args: 180 command: The name of the command, used for reporting. 181 cfg: An AcloudConfig instance. 182 device_factory: A factory capable of producing a single device. 183 num: The number of devices to create. 184 avd_type: String, the AVD type(cuttlefish, goldfish...). 185 report_internal_ip: Boolean to report the internal ip instead of 186 external ip. 187 serial_log_file: String, the file path to tar the serial logs. 188 autoconnect: Boolean, whether to auto connect to device. 189 client_adb_port: Integer, Specify port for adb forwarding. 190 boot_timeout_secs: Integer, boot timeout secs. 191 unlock_screen: Boolean, whether to unlock screen after invoke vnc client. 192 wait_for_boot: Boolean, True to check serial log include boot up 193 message. 194 195 Raises: 196 errors: Create instance fail. 197 198 Returns: 199 A Report instance. 200 """ 201 reporter = report.Report(command=command) 202 try: 203 CreateSshKeyPairIfNecessary(cfg) 204 device_pool = DevicePool(device_factory) 205 device_pool.CreateDevices(num) 206 device_pool.SetDeviceBuildInfo() 207 if wait_for_boot: 208 failures = device_pool.WaitForBoot(boot_timeout_secs) 209 else: 210 failures = device_factory.GetFailures() 211 212 if failures: 213 reporter.SetStatus(report.Status.BOOT_FAIL) 214 else: 215 reporter.SetStatus(report.Status.SUCCESS) 216 217 # Collect logs 218 if serial_log_file: 219 device_pool.CollectSerialPortLogs( 220 serial_log_file, port=constants.DEFAULT_SERIAL_PORT) 221 222 # Write result to report. 223 for device in device_pool.devices: 224 ip = (device.ip.internal if report_internal_ip 225 else device.ip.external) 226 device_dict = { 227 "ip": ip, 228 "instance_name": device.instance_name 229 } 230 if device.build_info: 231 device_dict.update(device.build_info) 232 if device.time_info: 233 device_dict.update(device.time_info) 234 if autoconnect: 235 forwarded_ports = utils.AutoConnect( 236 ip_addr=ip, 237 rsa_key_file=cfg.ssh_private_key_path, 238 target_vnc_port=utils.AVD_PORT_DICT[avd_type].vnc_port, 239 target_adb_port=utils.AVD_PORT_DICT[avd_type].adb_port, 240 ssh_user=constants.GCE_USER, 241 client_adb_port=client_adb_port, 242 extra_args_ssh_tunnel=cfg.extra_args_ssh_tunnel) 243 device_dict[constants.VNC_PORT] = forwarded_ports.vnc_port 244 device_dict[constants.ADB_PORT] = forwarded_ports.adb_port 245 if unlock_screen: 246 AdbTools(forwarded_ports.adb_port).AutoUnlockScreen() 247 if device.instance_name in failures: 248 reporter.AddData(key="devices_failing_boot", value=device_dict) 249 reporter.AddError(str(failures[device.instance_name])) 250 else: 251 reporter.AddData(key="devices", value=device_dict) 252 except errors.DriverError as e: 253 reporter.AddError(str(e)) 254 reporter.SetStatus(report.Status.FAIL) 255 return reporter 256