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"""A client that manages Goldfish Virtual Device on compute engine.
17
18** GoldfishComputeClient **
19
20GoldfishComputeClient derives from AndroidComputeClient. It manges a google
21compute engine project that is setup for running Goldfish Virtual Devices.
22It knows how to create a host instance from a Goldfish Stable Host Image, fetch
23Android build, an emulator build, and start Android within the host instance.
24
25** Class hierarchy **
26
27  base_cloud_client.BaseCloudApiClient
28                ^
29                |
30       gcompute_client.ComputeClient
31                ^
32                |
33       android_compute_client.AndroidComputeClient
34                ^
35                |
36       goldfish_compute_client.GoldfishComputeClient
37
38
39TODO: This class should likely be merged with CvdComputeClient
40"""
41
42import getpass
43import logging
44
45from acloud import errors
46from acloud.internal import constants
47from acloud.internal.lib import android_compute_client
48from acloud.internal.lib import gcompute_client
49
50
51logger = logging.getLogger(__name__)
52
53
54class GoldfishComputeClient(android_compute_client.AndroidComputeClient):
55    """Client that manages Goldfish based Android Virtual Device.
56
57    Attributes:
58        acloud_config: An AcloudConfig object.
59        oauth2_credentials: An oauth2client.OAuth2Credentials instance.
60    """
61
62    # To determine if the boot failed
63    BOOT_FAILED_MSG = "VIRTUAL_DEVICE_FAILED"
64
65    # To determine the failure reason
66    # If the emulator build is not available
67    EMULATOR_FETCH_FAILED_MSG = "EMULATOR_FETCH_FAILED"
68    # If the system image build is not available
69    ANDROID_FETCH_FAILED_MSG = "ANDROID_FETCH_FAILED"
70    # If the emulator could not boot in time
71    BOOT_TIMEOUT_MSG = "VIRTUAL_DEVICE_BOOT_FAILED"
72
73    #pylint: disable=signature-differs
74    def _GetDiskArgs(self, disk_name, image_name, image_project, disk_size_gb):
75        """Helper to generate disk args that is used to create an instance.
76
77        Args:
78            disk_name: String, the name of disk.
79            image_name: String, the name of the system image.
80            image_project: String, the name of the project where the image.
81            disk_size_gb: Integer, size of the blank data disk in GB.
82
83        Returns:
84            A dictionary representing disk args.
85        """
86        return [{
87            "type": "PERSISTENT",
88            "boot": True,
89            "mode": "READ_WRITE",
90            "autoDelete": True,
91            "initializeParams": {
92                "diskName":
93                disk_name,
94                "sourceImage":
95                self.GetImage(image_name, image_project)["selfLink"],
96                "diskSizeGb":
97                disk_size_gb
98            },
99        }]
100    #pylint: disable=signature-differs
101
102    def CheckBootFailure(self, serial_out, instance):
103        """Overriding method from the parent class.
104
105        Args:
106            serial_out: String
107            instance: String
108
109        Raises:
110            errors.DownloadArtifactError: If it fails to download artifact.
111            errors.DeviceBootError: If it fails to boot up.
112        """
113        if self.BOOT_FAILED_MSG in serial_out:
114            if self.EMULATOR_FETCH_FAILED_MSG in serial_out:
115                raise errors.DownloadArtifactError(
116                    "Failed to download emulator build. Re-run with a newer build."
117                )
118            if self.ANDROID_FETCH_FAILED_MSG in serial_out:
119                raise errors.DownloadArtifactError(
120                    "Failed to download system image build. Re-run with a newer build."
121                )
122            if self.BOOT_TIMEOUT_MSG in serial_out:
123                raise errors.DeviceBootError(
124                    "Emulator timed out while booting.")
125
126    @staticmethod
127    def GetKernelBuildArtifact(target):
128        """Get kernel build artifact name.
129
130        Args:
131            target: String, kernel target name.
132
133        Returns:
134            String of artifact name.
135
136        Raises:
137            errors.DeviceBootError: If it fails to get artifact name.
138        """
139        if target == "kernel":
140            return "bzImage"
141        if target == "kernel_x86_64":
142            return "bzImage"
143        if target == "kernel_aarch64":
144            return "Image.gz"
145        raise errors.DeviceBootError(
146            "Don't know the artifact name for '%s' target" % target)
147
148    # pylint: disable=too-many-locals,arguments-differ
149    # TODO: Refactor CreateInstance to pass in an object instead of all these args.
150    def CreateInstance(self,
151                       instance,
152                       image_name,
153                       image_project,
154                       build_target,
155                       branch,
156                       build_id,
157                       kernel_branch=None,
158                       kernel_build_id=None,
159                       kernel_build_target=None,
160                       emulator_branch=None,
161                       emulator_build_id=None,
162                       emulator_build_target=None,
163                       blank_data_disk_size_gb=None,
164                       gpu=None,
165                       avd_spec=None,
166                       extra_scopes=None,
167                       tags=None,
168                       launch_args=None):
169        """Create a goldfish instance given a stable host image and a build id.
170
171        Args:
172            instance: String, instance name.
173            image_name: String, the name of the system image.
174            image_project: String, name of the project where the image belongs.
175                           Assume the default project if None.
176            build_target: String, target name, e.g. "sdk_phone_x86_64-sdk"
177            branch: String, branch name, e.g. "git_pi-dev"
178            build_id: String, build id, a string, e.g. "2263051", "P2804227"
179            kernel_branch: String, kernel branch name.
180            kernel_build_id: String, kernel build id.
181            kernel_build_target: kernel target, e.g. "kernel_x86_64"
182            emulator_branch: String, emulator branch name, e.g."aosp-emu-master-dev"
183            emulator_build_id: String, emulator build id, a string, e.g. "2263051", "P2804227"
184            emulator_build_target: String, emulator build target.
185            blank_data_disk_size_gb: Integer, size of the blank data disk in GB.
186            gpu: String, GPU that should be attached to the instance, or None of no
187                 acceleration is needed. e.g. "nvidia-tesla-k80"
188            avd_spec: An AVDSpec instance.
189            extra_scopes: A list of extra scopes to be passed to the instance.
190            tags: A list of tags to associate with the instance. e.g.
191                 ["http-server", "https-server"]
192            launch_args: String of args for launch command.
193        """
194        self._VerifyZoneByQuota()
195        self._CheckMachineSize()
196
197        # Add space for possible data partition.
198        boot_disk_size_gb = (
199            int(self.GetImage(image_name, image_project)["diskSizeGb"]) +
200            blank_data_disk_size_gb)
201        disk_args = self._GetDiskArgs(instance, image_name, image_project,
202                                      boot_disk_size_gb)
203
204        # Goldfish instances are metadata compatible with cuttlefish devices.
205        # See details goto/goldfish-deployment
206        metadata = self._metadata.copy()
207        metadata["user"] = getpass.getuser()
208        metadata[constants.INS_KEY_AVD_TYPE] = constants.TYPE_GF
209
210        # Note that we use the same metadata naming conventions as cuttlefish
211        metadata["cvd_01_fetch_android_build_target"] = build_target
212        metadata["cvd_01_fetch_android_bid"] = "{branch}/{build_id}".format(
213            branch=branch, build_id=build_id)
214        if kernel_branch and kernel_build_id and kernel_build_target:
215            metadata["cvd_01_fetch_kernel_bid"] = "{branch}/{build_id}".format(
216                branch=kernel_branch, build_id=kernel_build_id)
217            metadata["cvd_01_fetch_kernel_build_target"] = kernel_build_target
218            metadata["cvd_01_fetch_kernel_build_artifact"] = (
219                self.GetKernelBuildArtifact(kernel_build_target))
220            metadata["cvd_01_use_custom_kernel"] = "true"
221        if emulator_branch and emulator_build_id:
222            metadata[
223                "cvd_01_fetch_emulator_bid"] = "{branch}/{build_id}".format(
224                    branch=emulator_branch, build_id=emulator_build_id)
225        if emulator_build_target:
226            metadata["cvd_01_fetch_emulator_build_target"] = emulator_build_target
227        if launch_args:
228            metadata["launch_args"] = launch_args
229        metadata["cvd_01_launch"] = "1"
230
231        # Update metadata by avd_spec
232        # for legacy create_gf cmd, we will keep using resolution.
233        # And always use avd_spec for acloud create cmd.
234        if avd_spec:
235            metadata[constants.INS_KEY_AVD_FLAVOR] = avd_spec.flavor
236            metadata["cvd_01_x_res"] = avd_spec.hw_property[constants.HW_X_RES]
237            metadata["cvd_01_y_res"] = avd_spec.hw_property[constants.HW_Y_RES]
238            metadata["cvd_01_dpi"] = avd_spec.hw_property[constants.HW_ALIAS_DPI]
239            metadata[constants.INS_KEY_DISPLAY] = ("%sx%s (%s)" % (
240                avd_spec.hw_property[constants.HW_X_RES],
241                avd_spec.hw_property[constants.HW_Y_RES],
242                avd_spec.hw_property[constants.HW_ALIAS_DPI]))
243        else:
244            resolution = self._resolution.split("x")
245            metadata["cvd_01_x_res"] = resolution[0]
246            metadata["cvd_01_y_res"] = resolution[1]
247            metadata["cvd_01_dpi"] = resolution[3]
248
249        gcompute_client.ComputeClient.CreateInstance(
250            self,
251            instance=instance,
252            image_name=image_name,
253            image_project=image_project,
254            disk_args=disk_args,
255            metadata=metadata,
256            machine_type=self._machine_type,
257            network=self._network,
258            zone=self._zone,
259            gpu=gpu,
260            tags=tags,
261            extra_scopes=extra_scopes)
262