• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2019 - The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""A client that manages Cuttlefish Virtual Device on compute engine.
15
16** CvdComputeClient **
17
18CvdComputeClient derives from AndroidComputeClient. It manges a google
19compute engine project that is setup for running Cuttlefish Virtual Devices.
20It knows how to create a host instance from Cuttlefish Stable Host Image, fetch
21Android build, and start Android within the host instance.
22
23** Class hierarchy **
24
25  base_cloud_client.BaseCloudApiClient
26                ^
27                |
28       gcompute_client.ComputeClient
29                ^
30                |
31       android_compute_client.AndroidComputeClient
32                ^
33                |
34       cvd_compute_client_multi_stage.CvdComputeClient
35
36"""
37
38import logging
39import os
40import subprocess
41import tempfile
42import time
43
44from acloud import errors
45from acloud.internal import constants
46from acloud.internal.lib import android_build_client
47from acloud.internal.lib import android_compute_client
48from acloud.internal.lib import cvd_utils
49from acloud.internal.lib import gcompute_client
50from acloud.internal.lib import utils
51from acloud.internal.lib.ssh import Ssh
52from acloud.setup import mkcert
53
54
55logger = logging.getLogger(__name__)
56
57_DEFAULT_WEBRTC_DEVICE_ID = "cvd-1"
58_TRUST_REMOTE_INSTANCE_COMMAND = (
59    f"\"sudo cp -p ~/{constants.WEBRTC_CERTS_PATH}/{constants.SSL_CA_NAME}.pem "
60    f"{constants.SSL_TRUST_CA_DIR}/{constants.SSL_CA_NAME}.crt;"
61    "sudo update-ca-certificates;\"")
62
63
64class CvdComputeClient(android_compute_client.AndroidComputeClient):
65    """Client that manages Android Virtual Device."""
66
67    DATA_POLICY_CREATE_IF_MISSING = "create_if_missing"
68    # Data policy to customize disk size.
69    DATA_POLICY_ALWAYS_CREATE = "always_create"
70
71    def __init__(self,
72                 acloud_config,
73                 oauth2_credentials,
74                 ins_timeout_secs=None,
75                 report_internal_ip=None,
76                 gpu=None):
77        """Initialize.
78
79        Args:
80            acloud_config: An AcloudConfig object.
81            oauth2_credentials: An oauth2client.OAuth2Credentials instance.
82            ins_timeout_secs: Integer, the maximum time to wait for the
83                              instance ready.
84            report_internal_ip: Boolean to report the internal ip instead of
85                                external ip.
86            gpu: String, GPU to attach to the device.
87        """
88        super().__init__(acloud_config, oauth2_credentials)
89
90        self._build_api = (
91            android_build_client.AndroidBuildClient(oauth2_credentials))
92        self._ssh_private_key_path = acloud_config.ssh_private_key_path
93        self._ins_timeout_secs = ins_timeout_secs
94        self._report_internal_ip = report_internal_ip
95        self._gpu = gpu
96        # Store all failures result when creating one or multiple instances.
97        # This attribute is only used by the deprecated create_cf command.
98        self._all_failures = {}
99        self._extra_args_ssh_tunnel = acloud_config.extra_args_ssh_tunnel
100        self._ssh = None
101        self._ip = None
102        self._user = constants.GCE_USER
103        self._openwrt = None
104        self._stage = constants.STAGE_INIT
105        self._execution_time = {constants.TIME_ARTIFACT: 0,
106                                constants.TIME_GCE: 0,
107                                constants.TIME_LAUNCH: 0}
108
109    # pylint: disable=arguments-differ,broad-except
110    def CreateInstance(self, instance, image_name, image_project,
111                       avd_spec, extra_scopes=None):
112        """Create/Reuse a GCE instance.
113
114        Args:
115            instance: instance name.
116            image_name: A string, the name of the GCE image.
117            image_project: A string, name of the project where the image lives.
118                           Assume the default project if None.
119            avd_spec: An AVDSpec instance.
120            extra_scopes: A list of extra scopes to be passed to the instance.
121
122        Returns:
123            A string, representing instance name.
124        """
125        # A blank data disk would be created on the host. Make sure the size of
126        # the boot disk is large enough to hold it.
127        boot_disk_size_gb = (
128            int(self.GetImage(image_name, image_project)["diskSizeGb"]) +
129            avd_spec.cfg.extra_data_disk_size_gb)
130
131        if avd_spec.instance_name_to_reuse:
132            self._ip = self._ReusingGceInstance(avd_spec)
133        else:
134            self._VerifyZoneByQuota()
135            self._ip = self._CreateGceInstance(instance, image_name, image_project,
136                                               extra_scopes, boot_disk_size_gb,
137                                               avd_spec)
138        if avd_spec.connect_hostname:
139            self._gce_hostname = gcompute_client.GetGCEHostName(
140                self._project, instance, self._zone)
141        self._ssh = Ssh(ip=self._ip,
142                        user=constants.GCE_USER,
143                        ssh_private_key_path=self._ssh_private_key_path,
144                        extra_args_ssh_tunnel=self._extra_args_ssh_tunnel,
145                        report_internal_ip=self._report_internal_ip,
146                        gce_hostname=self._gce_hostname)
147        try:
148            self.SetStage(constants.STAGE_SSH_CONNECT)
149            self._ssh.WaitForSsh(timeout=self._ins_timeout_secs)
150            if avd_spec.instance_name_to_reuse:
151                cvd_utils.CleanUpRemoteCvd(self._ssh, cvd_utils.GCE_BASE_DIR,
152                                           raise_error=False)
153        except Exception as e:
154            self._all_failures[instance] = e
155        return instance
156
157    def _GetGCEHostName(self, instance):
158        """Get the GCE host name with specific rule.
159
160        Args:
161            instance: Sting, instance name.
162
163        Returns:
164            One host name coverted by instance name, project name, and zone.
165        """
166        if ":" in self._project:
167            domain = self._project.split(":")[0]
168            project_no_domain = self._project.split(":")[1]
169            project = f"{project_no_domain}.{domain}"
170            return f"nic0.{instance}.{self._zone}.c.{project}.internal.gcpnode.com"
171        return f"nic0.{instance}.{self._zone}.c.{self._project}.internal.gcpnode.com"
172
173    @utils.TimeExecute(function_description="Launching AVD(s) and waiting for boot up",
174                       result_evaluator=utils.BootEvaluator)
175    def LaunchCvd(self, instance, avd_spec, base_dir, extra_args):
176        """Launch CVD.
177
178        Launch AVD with launch_cvd. If the process is failed, acloud would show
179        error messages and auto download log files from remote instance.
180
181        Args:
182            instance: String, instance name.
183            avd_spec: An AVDSpec instance.
184            base_dir: The remote directory containing the images and tools.
185            extra_args: Collection of strings, the extra arguments generated by
186                        acloud. e.g., remote image paths.
187
188        Returns:
189           dict of faliures, return this dict for BootEvaluator to handle
190           LaunchCvd success or fail messages.
191        """
192        self.SetStage(constants.STAGE_BOOT_UP)
193        timestart = time.time()
194        config = cvd_utils.GetConfigFromRemoteAndroidInfo(self._ssh, base_dir)
195        cmd = cvd_utils.GetRemoteLaunchCvdCmd(
196            base_dir, avd_spec, config, extra_args)
197        boot_timeout_secs = self._GetBootTimeout(
198            avd_spec.boot_timeout_secs or constants.DEFAULT_CF_BOOT_TIMEOUT)
199
200        self.ExtendReportData(constants.LAUNCH_CVD_COMMAND, cmd)
201        error_msg = cvd_utils.ExecuteRemoteLaunchCvd(
202            self._ssh, cmd, boot_timeout_secs)
203        self._execution_time[constants.TIME_LAUNCH] = time.time() - timestart
204
205        if error_msg:
206            return {instance: error_msg}
207        self._openwrt = avd_spec.openwrt
208        return {}
209
210    def _GetBootTimeout(self, timeout_secs):
211        """Get boot timeout.
212
213        Timeout settings includes download artifacts and boot up.
214
215        Args:
216            timeout_secs: integer of timeout value.
217
218        Returns:
219            The timeout values for device boots up.
220        """
221        boot_timeout_secs = timeout_secs - self._execution_time[constants.TIME_ARTIFACT]
222        logger.debug("Timeout for boot: %s secs", boot_timeout_secs)
223        return boot_timeout_secs
224
225    @utils.TimeExecute(function_description="Reusing GCE instance")
226    def _ReusingGceInstance(self, avd_spec):
227        """Reusing a cuttlefish existing instance.
228
229        Args:
230            avd_spec: An AVDSpec instance.
231
232        Returns:
233            ssh.IP object, that stores internal and external ip of the instance.
234        """
235        gcompute_client.ComputeClient.AddSshRsaInstanceMetadata(
236            self, constants.GCE_USER, avd_spec.cfg.ssh_public_key_path,
237            avd_spec.instance_name_to_reuse)
238        ip = gcompute_client.ComputeClient.GetInstanceIP(
239            self, instance=avd_spec.instance_name_to_reuse, zone=self._zone)
240
241        return ip
242
243    @utils.TimeExecute(function_description="Creating GCE instance")
244    def _CreateGceInstance(self, instance, image_name, image_project,
245                           extra_scopes, boot_disk_size_gb, avd_spec):
246        """Create a single configured cuttlefish device.
247
248        Override method from parent class.
249        Args:
250            instance: String, instance name.
251            image_name: String, the name of the GCE image.
252            image_project: String, the name of the project where the image.
253            extra_scopes: A list of extra scopes to be passed to the instance.
254            boot_disk_size_gb: Integer, size of the boot disk in GB.
255            avd_spec: An AVDSpec instance.
256
257        Returns:
258            ssh.IP object, that stores internal and external ip of the instance.
259        """
260        self.SetStage(constants.STAGE_GCE)
261        timestart = time.time()
262        metadata = self._metadata.copy()
263
264        if avd_spec:
265            metadata[constants.INS_KEY_AVD_TYPE] = avd_spec.avd_type
266            metadata[constants.INS_KEY_AVD_FLAVOR] = avd_spec.flavor
267            metadata[constants.INS_KEY_WEBRTC_DEVICE_ID] = (
268                avd_spec.webrtc_device_id or _DEFAULT_WEBRTC_DEVICE_ID)
269            metadata[constants.INS_KEY_DISPLAY] = ("%sx%s (%s)" % (
270                avd_spec.hw_property[constants.HW_X_RES],
271                avd_spec.hw_property[constants.HW_Y_RES],
272                avd_spec.hw_property[constants.HW_ALIAS_DPI]))
273            if avd_spec.gce_metadata:
274                for key, value in avd_spec.gce_metadata.items():
275                    metadata[key] = value
276            # Record webrtc port, it will be removed if cvd support to show it.
277            if avd_spec.connect_webrtc:
278                metadata[constants.INS_KEY_WEBRTC_PORT] = constants.WEBRTC_LOCAL_PORT
279
280        disk_args = self._GetDiskArgs(
281            instance, image_name, image_project, boot_disk_size_gb)
282        disable_external_ip = avd_spec.disable_external_ip if avd_spec else False
283        gcompute_client.ComputeClient.CreateInstance(
284            self,
285            instance=instance,
286            image_name=image_name,
287            image_project=image_project,
288            disk_args=disk_args,
289            metadata=metadata,
290            machine_type=self._machine_type,
291            network=self._network,
292            zone=self._zone,
293            gpu=self._gpu,
294            disk_type=avd_spec.disk_type if avd_spec else None,
295            extra_scopes=extra_scopes,
296            disable_external_ip=disable_external_ip)
297        ip = gcompute_client.ComputeClient.GetInstanceIP(
298            self, instance=instance, zone=self._zone)
299        logger.debug("'instance_ip': %s", ip.internal
300                     if self._report_internal_ip else ip.external)
301
302        self._execution_time[constants.TIME_GCE] = time.time() - timestart
303        return ip
304
305    @utils.TimeExecute(function_description="Downloading build on instance")
306    def FetchBuild(self, default_build_info, system_build_info,
307                   kernel_build_info, boot_build_info, bootloader_build_info,
308                   android_efi_loader_build_info, ota_build_info, host_package_build_info):
309        """Execute fetch_cvd on the remote instance to get Cuttlefish runtime files.
310
311        Args:
312            default_build_info: The build that provides full cuttlefish images.
313            system_build_info: The build that provides the system image.
314            kernel_build_info: The build that provides the kernel.
315            boot_build_info: The build that provides the boot image.
316            bootloader_build_info: The build that provides the bootloader.
317            android_efi_loader_build_info: The build that provides the Android EFI app.
318            ota_build_info: The build that provides the OTA tools.
319            host_package_build_info: The build that provides the host package.
320
321        Returns:
322            List of string args for fetch_cvd.
323        """
324        self.SetStage(constants.STAGE_ARTIFACT)
325        timestart = time.time()
326        cmd = list(constants.CMD_CVD_FETCH) + ["-credential_source=gce"]
327        fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
328            default_build_info, system_build_info, kernel_build_info,
329            boot_build_info, bootloader_build_info, android_efi_loader_build_info,
330            ota_build_info, host_package_build_info)
331        cmd.extend(fetch_cvd_build_args)
332
333        self._ssh.Run(" ".join(cmd),
334                      timeout=constants.DEFAULT_SSH_TIMEOUT)
335        self._execution_time[constants.TIME_ARTIFACT] = time.time() - timestart
336
337    @utils.TimeExecute(function_description="Update instance's certificates")
338    def UpdateCertificate(self):
339        """Update webrtc default certificates of the remote instance.
340
341        For trusting both the localhost and remote instance, the process will
342        upload certificates(rootCA.pem, server.crt, server.key) and the mkcert
343        tool from the client workstation to remote instance where running the
344        mkcert with the uploaded rootCA file and replace the webrtc frontend
345        default certificates for connecting to a remote webrtc AVD without the
346        insecure warning.
347        """
348        local_cert_dir = os.path.join(os.path.expanduser("~"),
349                                      constants.SSL_DIR)
350        if mkcert.AllocateLocalHostCert():
351            upload_files = []
352            for cert_file in (constants.WEBRTC_CERTS_FILES +
353                              [f"{constants.SSL_CA_NAME}.pem"]):
354                upload_files.append(os.path.join(local_cert_dir,
355                                                 cert_file))
356            try:
357                self._ssh.ScpPushFiles(upload_files, constants.WEBRTC_CERTS_PATH)
358                self._ssh.Run(_TRUST_REMOTE_INSTANCE_COMMAND)
359            except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
360                logger.debug("Update WebRTC frontend certificate failed.")
361
362    @utils.TimeExecute(function_description="Upload extra files to instance")
363    def UploadExtraFiles(self, extra_files):
364        """Upload extra files into GCE instance.
365
366        Args:
367            extra_files: List of namedtuple ExtraFile.
368
369        Raises:
370            errors.CheckPathError: The provided path doesn't exist.
371        """
372        for extra_file in extra_files:
373            if not os.path.exists(extra_file.source):
374                raise errors.CheckPathError(
375                    f"The path doesn't exist: {extra_file.source}")
376            self._ssh.ScpPushFile(extra_file.source, extra_file.target)
377
378    def GetSshConnectCmd(self):
379        """Get ssh connect command.
380
381        Returns:
382            String of ssh connect command.
383        """
384        return self._ssh.GetBaseCmd(constants.SSH_BIN)
385
386    def GetInstanceIP(self, instance=None):
387        """Override method from parent class.
388
389        It need to get the IP address in the common_operation. If the class
390        already defind the ip address, return the ip address.
391
392        Args:
393            instance: String, representing instance name.
394
395        Returns:
396            ssh.IP object, that stores internal and external ip of the instance.
397        """
398        if self._ip:
399            return self._ip
400        return gcompute_client.ComputeClient.GetInstanceIP(
401            self, instance=instance, zone=self._zone)
402
403    def GetHostImageName(self, stable_image_name, image_family, image_project):
404        """Get host image name.
405
406        Args:
407            stable_image_name: String of stable host image name.
408            image_family: String of image family.
409            image_project: String of image project.
410
411        Returns:
412            String of stable host image name.
413
414        Raises:
415            errors.ConfigError: There is no host image name in config file.
416        """
417        if stable_image_name:
418            return stable_image_name
419
420        if image_family:
421            image_name = gcompute_client.ComputeClient.GetImageFromFamily(
422                self, image_family, image_project)["name"]
423            logger.debug("Get the host image name from image family: %s", image_name)
424            return image_name
425
426        raise errors.ConfigError(
427            "Please specify 'stable_host_image_name' or 'stable_host_image_family'"
428            " in config.")
429
430    def SetStage(self, stage):
431        """Set stage to know the create progress.
432
433        Args:
434            stage: Integer, the stage would like STAGE_INIT, STAGE_GCE.
435        """
436        self._stage = stage
437
438    @property
439    def all_failures(self):
440        """Return all_failures"""
441        return self._all_failures
442
443    @property
444    def execution_time(self):
445        """Return execution_time"""
446        return self._execution_time
447
448    @property
449    def stage(self):
450        """Return stage"""
451        return self._stage
452
453    @property
454    def openwrt(self):
455        """Return openwrt"""
456        return self._openwrt
457
458    @property
459    def build_api(self):
460        """Return build_api"""
461        return self._build_api
462