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