• 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 re
41import subprocess
42import tempfile
43import time
44
45from acloud import errors
46from acloud.internal import constants
47from acloud.internal.lib import android_build_client
48from acloud.internal.lib import android_compute_client
49from acloud.internal.lib import cvd_utils
50from acloud.internal.lib import gcompute_client
51from acloud.internal.lib import utils
52from acloud.internal.lib.ssh import Ssh
53from acloud.setup import mkcert
54
55
56logger = logging.getLogger(__name__)
57
58_CONFIG_ARG = "-config"
59_DECOMPRESS_KERNEL_ARG = "-decompress_kernel=true"
60_AGREEMENT_PROMPT_ARG = "-report_anonymous_usage_stats=y"
61_UNDEFOK_ARG = "-undefok=report_anonymous_usage_stats,config"
62_NUM_AVDS_ARG = "-num_instances=%(num_AVD)s"
63# Connect the OpenWrt device via console file.
64_ENABLE_CONSOLE_ARG = "-console=true"
65_DEFAULT_BRANCH = "aosp-master"
66_FETCHER_BUILD_TARGET = "aosp_cf_x86_64_phone-userdebug"
67_FETCHER_NAME = "fetch_cvd"
68# Time info to write in report.
69_FETCH_ARTIFACT = "fetch_artifact_time"
70_GCE_CREATE = "gce_create_time"
71_LAUNCH_CVD = "launch_cvd_time"
72# WebRTC args for launching AVD
73_START_WEBRTC = "--start_webrtc"
74_WEBRTC_ID = "--webrtc_device_id=%(instance)s"
75_VM_MANAGER = "--vm_manager=crosvm"
76_WEBRTC_ARGS = [_START_WEBRTC, _VM_MANAGER]
77_VNC_ARGS = ["--start_vnc_server=true"]
78_NO_RETRY = 0
79# Launch cvd command for acloud report
80_LAUNCH_CVD_COMMAND = "launch_cvd_command"
81_CONFIG_RE = re.compile(r"^config=(?P<config>.+)")
82_TRUST_REMOTE_INSTANCE_COMMAND = (
83    f"\"sudo cp -p ~/{constants.WEBRTC_CERTS_PATH}/{constants.SSL_CA_NAME}.pem "
84    f"{constants.SSL_TRUST_CA_DIR}/{constants.SSL_CA_NAME}.crt;"
85    "sudo update-ca-certificates;\"")
86# Remote host instance name
87_HOST_INSTANCE_NAME_FORMAT = (constants.INSTANCE_TYPE_HOST +
88                              "-%(ip_addr)s-%(build_id)s-%(build_target)s")
89_HOST_INSTANCE_NAME_PATTERN = re.compile(constants.INSTANCE_TYPE_HOST +
90                                         r"-(?P<ip_addr>[\d.]+)-.+")
91
92
93class CvdComputeClient(android_compute_client.AndroidComputeClient):
94    """Client that manages Android Virtual Device."""
95
96    DATA_POLICY_CREATE_IF_MISSING = "create_if_missing"
97    # Data policy to customize disk size.
98    DATA_POLICY_ALWAYS_CREATE = "always_create"
99
100    def __init__(self,
101                 acloud_config,
102                 oauth2_credentials,
103                 boot_timeout_secs=None,
104                 ins_timeout_secs=None,
105                 report_internal_ip=None,
106                 gpu=None):
107        """Initialize.
108
109        Args:
110            acloud_config: An AcloudConfig object.
111            oauth2_credentials: An oauth2client.OAuth2Credentials instance.
112            boot_timeout_secs: Integer, the maximum time to wait for the AVD
113                               to boot up.
114            ins_timeout_secs: Integer, the maximum time to wait for the
115                              instance ready.
116            report_internal_ip: Boolean to report the internal ip instead of
117                                external ip.
118            gpu: String, GPU to attach to the device.
119        """
120        super().__init__(acloud_config, oauth2_credentials)
121
122        self._fetch_cvd_version = acloud_config.fetch_cvd_version
123        self._build_api = (
124            android_build_client.AndroidBuildClient(oauth2_credentials))
125        self._ssh_private_key_path = acloud_config.ssh_private_key_path
126        self._boot_timeout_secs = boot_timeout_secs
127        self._ins_timeout_secs = ins_timeout_secs
128        self._report_internal_ip = report_internal_ip
129        self._gpu = gpu
130        # Store all failures result when creating one or multiple instances.
131        # This attribute is only used by the deprecated create_cf command.
132        self._all_failures = {}
133        self._extra_args_ssh_tunnel = acloud_config.extra_args_ssh_tunnel
134        self._ssh = None
135        self._ip = None
136        self._user = constants.GCE_USER
137        self._openwrt = None
138        self._stage = constants.STAGE_INIT
139        self._execution_time = {_FETCH_ARTIFACT: 0, _GCE_CREATE: 0, _LAUNCH_CVD: 0}
140
141    @staticmethod
142    def FormatRemoteHostInstanceName(ip_addr, build_id, build_target):
143        """Convert an IP address and build info to an instance name.
144
145        Args:
146            ip_addr: String, the IP address of the remote host.
147            build_id: String, the build id.
148            build_target: String, the build target, e.g., aosp_cf_x86_64_phone.
149
150        Return:
151            String, the instance name.
152        """
153        return _HOST_INSTANCE_NAME_FORMAT % {
154            "ip_addr": ip_addr,
155            "build_id": build_id,
156            "build_target": build_target}
157
158    @staticmethod
159    def ParseRemoteHostAddress(instance_name):
160        """Parse IP address from a remote host instance name.
161
162        Args:
163            instance_name: String, the instance name.
164
165        Returns:
166            The IP address as a string.
167            None if the name does not represent a remote host instance.
168        """
169        match = _HOST_INSTANCE_NAME_PATTERN.fullmatch(instance_name)
170        return match.group("ip_addr") if match else None
171
172    def InitRemoteHost(self, ssh, ip, user):
173        """Init remote host.
174
175        Check if we can ssh to the remote host, stop any cf instances running
176        on it, and remove existing files.
177
178        Args:
179            ssh: Ssh object.
180            ip: namedtuple (internal, external) that holds IP address of the
181                remote host, e.g. "external:140.110.20.1, internal:10.0.0.1"
182            user: String of user log in to the instance.
183        """
184        self.SetStage(constants.STAGE_SSH_CONNECT)
185        self._ssh = ssh
186        self._ip = ip
187        self._user = user
188        self._ssh.WaitForSsh(timeout=self._ins_timeout_secs)
189        cvd_utils.CleanUpRemoteCvd(self._ssh, raise_error=False)
190
191    # TODO(171376263): Refactor CreateInstance() args with avd_spec.
192    # pylint: disable=arguments-differ,too-many-locals,broad-except
193    def CreateInstance(self, instance, image_name, image_project,
194                       build_target=None, branch=None, build_id=None,
195                       kernel_branch=None, kernel_build_id=None,
196                       kernel_build_target=None, blank_data_disk_size_gb=None,
197                       avd_spec=None, extra_scopes=None,
198                       system_build_target=None, system_branch=None,
199                       system_build_id=None, bootloader_build_target=None,
200                       bootloader_branch=None, bootloader_build_id=None,
201                       ota_build_target=None, ota_branch=None,
202                       ota_build_id=None):
203
204        """Create/Reuse a single configured cuttlefish device.
205        1. Prepare GCE instance.
206           Create a new instnace or get IP address for reusing the specific instance.
207        2. Put fetch_cvd on the instance.
208        3. Invoke fetch_cvd to fetch and run the instance.
209
210        Args:
211            instance: instance name.
212            image_name: A string, the name of the GCE image.
213            image_project: A string, name of the project where the image lives.
214                           Assume the default project if None.
215            build_target: Target name, e.g. "aosp_cf_x86_64_phone-userdebug"
216            branch: Branch name, e.g. "aosp-master"
217            build_id: Build id, a string, e.g. "2263051", "P2804227"
218            kernel_branch: Kernel branch name, e.g. "kernel-common-android-4.14"
219            kernel_build_id: Kernel build id, a string, e.g. "223051", "P280427"
220            kernel_build_target: String, Kernel build target name.
221            blank_data_disk_size_gb: Size of the blank data disk in GB.
222            avd_spec: An AVDSpec instance.
223            extra_scopes: A list of extra scopes to be passed to the instance.
224            system_build_target: String of the system image target name,
225                                 e.g. "cf_x86_phone-userdebug"
226            system_branch: String of the system image branch name.
227            system_build_id: String of the system image build id.
228            bootloader_build_target: String of the bootloader target name.
229            bootloader_branch: String of the bootloader branch name.
230            bootloader_build_id: String of the bootloader build id.
231            ota_build_target: String of the otatools target name.
232            ota_branch: String of the otatools branch name.
233            ota_build_id: String of the otatools build id.
234
235        Returns:
236            A string, representing instance name.
237        """
238
239        # A blank data disk would be created on the host. Make sure the size of
240        # the boot disk is large enough to hold it.
241        boot_disk_size_gb = (
242            int(self.GetImage(image_name, image_project)["diskSizeGb"]) +
243            blank_data_disk_size_gb)
244
245        if avd_spec and avd_spec.instance_name_to_reuse:
246            self._ip = self._ReusingGceInstance(avd_spec)
247        else:
248            self._VerifyZoneByQuota()
249            self._ip = self._CreateGceInstance(instance, image_name, image_project,
250                                               extra_scopes, boot_disk_size_gb,
251                                               avd_spec)
252        self._ssh = Ssh(ip=self._ip,
253                        user=constants.GCE_USER,
254                        ssh_private_key_path=self._ssh_private_key_path,
255                        extra_args_ssh_tunnel=self._extra_args_ssh_tunnel,
256                        report_internal_ip=self._report_internal_ip)
257        try:
258            self.SetStage(constants.STAGE_SSH_CONNECT)
259            self._ssh.WaitForSsh(timeout=self._ins_timeout_secs)
260            if avd_spec:
261                if avd_spec.instance_name_to_reuse:
262                    cvd_utils.CleanUpRemoteCvd(self._ssh, raise_error=False)
263                return instance
264
265            # TODO: Remove following code after create_cf deprecated.
266            self.UpdateFetchCvd()
267
268            self.FetchBuild(build_id, branch, build_target, system_build_id,
269                            system_branch, system_build_target, kernel_build_id,
270                            kernel_branch, kernel_build_target, bootloader_build_id,
271                            bootloader_branch, bootloader_build_target,
272                            ota_build_id, ota_branch, ota_build_target)
273            failures = self.LaunchCvd(
274                instance,
275                blank_data_disk_size_gb=blank_data_disk_size_gb,
276                boot_timeout_secs=self._boot_timeout_secs,
277                extra_args=[])
278            self._all_failures.update(failures)
279            return instance
280        except Exception as e:
281            self._all_failures[instance] = e
282            return instance
283
284    def _GetConfigFromAndroidInfo(self):
285        """Get config value from android-info.txt.
286
287        The config in android-info.txt would like "config=phone".
288
289        Returns:
290            Strings of config value.
291        """
292        android_info = self._ssh.GetCmdOutput(
293            "cat %s" % constants.ANDROID_INFO_FILE)
294        logger.debug("Android info: %s", android_info)
295        config_match = _CONFIG_RE.match(android_info)
296        if config_match:
297            return config_match.group("config")
298        return None
299
300    # pylint: disable=too-many-branches
301    def _GetLaunchCvdArgs(self, avd_spec=None, blank_data_disk_size_gb=None,
302                          decompress_kernel=None, instance=None):
303        """Get launch_cvd args.
304
305        Args:
306            avd_spec: An AVDSpec instance.
307            blank_data_disk_size_gb: Size of the blank data disk in GB.
308            decompress_kernel: Boolean, if true decompress the kernel.
309            instance: String, instance name.
310
311        Returns:
312            String, args of launch_cvd.
313        """
314        launch_cvd_args = []
315        if blank_data_disk_size_gb and blank_data_disk_size_gb > 0:
316            # Policy 'create_if_missing' would create a blank userdata disk if
317            # missing. If already exist, reuse the disk.
318            launch_cvd_args.append(
319                "-data_policy=" + self.DATA_POLICY_CREATE_IF_MISSING)
320            launch_cvd_args.append(
321                "-blank_data_image_mb=%d" % (blank_data_disk_size_gb * 1024))
322        if avd_spec:
323            config = self._GetConfigFromAndroidInfo()
324            if config:
325                launch_cvd_args.append("-config=%s" % config)
326            if avd_spec.hw_customize or not config:
327                launch_cvd_args.append(
328                    "-x_res=" + avd_spec.hw_property[constants.HW_X_RES])
329                launch_cvd_args.append(
330                    "-y_res=" + avd_spec.hw_property[constants.HW_Y_RES])
331                launch_cvd_args.append(
332                    "-dpi=" + avd_spec.hw_property[constants.HW_ALIAS_DPI])
333                if constants.HW_ALIAS_DISK in avd_spec.hw_property:
334                    launch_cvd_args.append(
335                        "-data_policy=" + self.DATA_POLICY_ALWAYS_CREATE)
336                    launch_cvd_args.append(
337                        "-blank_data_image_mb="
338                        + avd_spec.hw_property[constants.HW_ALIAS_DISK])
339                if constants.HW_ALIAS_CPUS in avd_spec.hw_property:
340                    launch_cvd_args.append(
341                        "-cpus=%s" % avd_spec.hw_property[constants.HW_ALIAS_CPUS])
342                if constants.HW_ALIAS_MEMORY in avd_spec.hw_property:
343                    launch_cvd_args.append(
344                        "-memory_mb=%s" % avd_spec.hw_property[constants.HW_ALIAS_MEMORY])
345            if avd_spec.connect_webrtc:
346                launch_cvd_args.extend(_WEBRTC_ARGS)
347                launch_cvd_args.append(_WEBRTC_ID % {"instance": instance})
348            if avd_spec.connect_vnc:
349                launch_cvd_args.extend(_VNC_ARGS)
350            if avd_spec.openwrt:
351                launch_cvd_args.append(_ENABLE_CONSOLE_ARG)
352            if avd_spec.num_avds_per_instance > 1:
353                launch_cvd_args.append(
354                    _NUM_AVDS_ARG % {"num_AVD": avd_spec.num_avds_per_instance})
355            if avd_spec.base_instance_num:
356                launch_cvd_args.append(
357                    "--base-instance-num=%s" % avd_spec.base_instance_num)
358            if avd_spec.launch_args:
359                launch_cvd_args.append(avd_spec.launch_args)
360        else:
361            resolution = self._resolution.split("x")
362            launch_cvd_args.append("-x_res=" + resolution[0])
363            launch_cvd_args.append("-y_res=" + resolution[1])
364            launch_cvd_args.append("-dpi=" + resolution[3])
365
366        if not avd_spec and self._launch_args:
367            launch_cvd_args.append(self._launch_args)
368
369        if decompress_kernel:
370            launch_cvd_args.append(_DECOMPRESS_KERNEL_ARG)
371
372        launch_cvd_args.append(_UNDEFOK_ARG)
373        launch_cvd_args.append(_AGREEMENT_PROMPT_ARG)
374        return launch_cvd_args
375
376    @utils.TimeExecute(function_description="Launching AVD(s) and waiting for boot up",
377                       result_evaluator=utils.BootEvaluator)
378    def LaunchCvd(self, instance, avd_spec=None,
379                  blank_data_disk_size_gb=None,
380                  decompress_kernel=None,
381                  boot_timeout_secs=None,
382                  extra_args=()):
383        """Launch CVD.
384
385        Launch AVD with launch_cvd. If the process is failed, acloud would show
386        error messages and auto download log files from remote instance.
387
388        Args:
389            instance: String, instance name.
390            avd_spec: An AVDSpec instance.
391            blank_data_disk_size_gb: Size of the blank data disk in GB.
392            decompress_kernel: Boolean, if true decompress the kernel.
393            boot_timeout_secs: Integer, the maximum time to wait for the
394                               command to respond.
395            extra_args: Collection of strings, the extra arguments generated by
396                        acloud. e.g., remote image paths.
397
398        Returns:
399           dict of faliures, return this dict for BootEvaluator to handle
400           LaunchCvd success or fail messages.
401        """
402        self.SetStage(constants.STAGE_BOOT_UP)
403        timestart = time.time()
404        error_msg = ""
405        launch_cvd_args = list(extra_args)
406        launch_cvd_args.extend(
407            self._GetLaunchCvdArgs(avd_spec, blank_data_disk_size_gb,
408                                   decompress_kernel, instance))
409        boot_timeout_secs = self._GetBootTimeout(
410            boot_timeout_secs or constants.DEFAULT_CF_BOOT_TIMEOUT)
411        ssh_command = "./bin/launch_cvd -daemon " + " ".join(launch_cvd_args)
412        try:
413            if avd_spec and avd_spec.base_instance_num:
414                self.ExtendReportData(constants.BASE_INSTANCE_NUM, avd_spec.base_instance_num)
415            self.ExtendReportData(_LAUNCH_CVD_COMMAND, ssh_command)
416            self._ssh.Run(ssh_command, boot_timeout_secs, retry=_NO_RETRY)
417            self._UpdateOpenWrtStatus(avd_spec)
418        except (subprocess.CalledProcessError, errors.DeviceConnectionError,
419                errors.LaunchCVDFail) as e:
420            error_msg = ("Device %s did not finish on boot within timeout (%s secs)"
421                         % (instance, boot_timeout_secs))
422            if constants.ERROR_MSG_VNC_NOT_SUPPORT in str(e):
423                error_msg = (
424                    "VNC is not supported in the current build. Please try WebRTC such "
425                    "as '$acloud create' or '$acloud create --autoconnect webrtc'")
426            if constants.ERROR_MSG_WEBRTC_NOT_SUPPORT in str(e):
427                error_msg = (
428                    "WEBRTC is not supported in the current build. Please try VNC such "
429                    "as '$acloud create --autoconnect vnc'")
430            utils.PrintColorString(str(e), utils.TextColors.FAIL)
431
432        self._execution_time[_LAUNCH_CVD] = round(time.time() - timestart, 2)
433        return {instance: error_msg} if error_msg else {}
434
435    def _GetBootTimeout(self, timeout_secs):
436        """Get boot timeout.
437
438        Timeout settings includes download artifacts and boot up.
439
440        Args:
441            timeout_secs: integer of timeout value.
442
443        Returns:
444            The timeout values for device boots up.
445        """
446        boot_timeout_secs = timeout_secs - self._execution_time[_FETCH_ARTIFACT]
447        logger.debug("Timeout for boot: %s secs", boot_timeout_secs)
448        return boot_timeout_secs
449
450    @utils.TimeExecute(function_description="Reusing GCE instance")
451    def _ReusingGceInstance(self, avd_spec):
452        """Reusing a cuttlefish existing instance.
453
454        Args:
455            avd_spec: An AVDSpec instance.
456
457        Returns:
458            ssh.IP object, that stores internal and external ip of the instance.
459        """
460        gcompute_client.ComputeClient.AddSshRsaInstanceMetadata(
461            self, constants.GCE_USER, avd_spec.cfg.ssh_public_key_path,
462            avd_spec.instance_name_to_reuse)
463        ip = gcompute_client.ComputeClient.GetInstanceIP(
464            self, instance=avd_spec.instance_name_to_reuse, zone=self._zone)
465
466        return ip
467
468    @utils.TimeExecute(function_description="Creating GCE instance")
469    def _CreateGceInstance(self, instance, image_name, image_project,
470                           extra_scopes, boot_disk_size_gb, avd_spec):
471        """Create a single configured cuttlefish device.
472
473        Override method from parent class.
474        Args:
475            instance: String, instance name.
476            image_name: String, the name of the GCE image.
477            image_project: String, the name of the project where the image.
478            extra_scopes: A list of extra scopes to be passed to the instance.
479            boot_disk_size_gb: Integer, size of the boot disk in GB.
480            avd_spec: An AVDSpec instance.
481
482        Returns:
483            ssh.IP object, that stores internal and external ip of the instance.
484        """
485        self.SetStage(constants.STAGE_GCE)
486        timestart = time.time()
487        metadata = self._metadata.copy()
488
489        if avd_spec:
490            metadata[constants.INS_KEY_AVD_TYPE] = avd_spec.avd_type
491            metadata[constants.INS_KEY_AVD_FLAVOR] = avd_spec.flavor
492            metadata[constants.INS_KEY_DISPLAY] = ("%sx%s (%s)" % (
493                avd_spec.hw_property[constants.HW_X_RES],
494                avd_spec.hw_property[constants.HW_Y_RES],
495                avd_spec.hw_property[constants.HW_ALIAS_DPI]))
496            if avd_spec.gce_metadata:
497                for key, value in avd_spec.gce_metadata.items():
498                    metadata[key] = value
499            # Record webrtc port, it will be removed if cvd support to show it.
500            if avd_spec.connect_webrtc:
501                metadata[constants.INS_KEY_WEBRTC_PORT] = constants.WEBRTC_LOCAL_PORT
502
503        disk_args = self._GetDiskArgs(
504            instance, image_name, image_project, boot_disk_size_gb)
505        disable_external_ip = avd_spec.disable_external_ip if avd_spec else False
506        gcompute_client.ComputeClient.CreateInstance(
507            self,
508            instance=instance,
509            image_name=image_name,
510            image_project=image_project,
511            disk_args=disk_args,
512            metadata=metadata,
513            machine_type=self._machine_type,
514            network=self._network,
515            zone=self._zone,
516            gpu=self._gpu,
517            disk_type=avd_spec.disk_type if avd_spec else None,
518            extra_scopes=extra_scopes,
519            disable_external_ip=disable_external_ip)
520        ip = gcompute_client.ComputeClient.GetInstanceIP(
521            self, instance=instance, zone=self._zone)
522        logger.debug("'instance_ip': %s", ip.internal
523                     if self._report_internal_ip else ip.external)
524
525        self._execution_time[_GCE_CREATE] = round(time.time() - timestart, 2)
526        return ip
527
528    @utils.TimeExecute(function_description="Uploading build fetcher to instance")
529    def UpdateFetchCvd(self):
530        """Download fetch_cvd from the Build API, and upload it to a remote instance.
531
532        The version of fetch_cvd to use is retrieved from the configuration file. Once fetch_cvd
533        is on the instance, future commands can use it to download relevant Cuttlefish files from
534        the Build API on the instance itself.
535        """
536        self.SetStage(constants.STAGE_ARTIFACT)
537        download_dir = tempfile.mkdtemp()
538        download_target = os.path.join(download_dir, _FETCHER_NAME)
539        self._build_api.DownloadFetchcvd(download_target, self._fetch_cvd_version)
540        self._ssh.ScpPushFile(src_file=download_target, dst_file=_FETCHER_NAME)
541        os.remove(download_target)
542        os.rmdir(download_dir)
543
544    @utils.TimeExecute(function_description="Downloading build on instance")
545    def FetchBuild(self, build_id, branch, build_target, system_build_id,
546                   system_branch, system_build_target, kernel_build_id,
547                   kernel_branch, kernel_build_target, bootloader_build_id,
548                   bootloader_branch, bootloader_build_target, ota_build_id,
549                   ota_branch, ota_build_target):
550        """Execute fetch_cvd on the remote instance to get Cuttlefish runtime files.
551
552        Args:
553            build_id: String of build id, e.g. "2263051", "P2804227"
554            branch: String of branch name, e.g. "aosp-master"
555            build_target: String of target name.
556                          e.g. "aosp_cf_x86_64_phone-userdebug"
557            system_build_id: String of the system image build id.
558            system_branch: String of the system image branch name.
559            system_build_target: String of the system image target name,
560                                 e.g. "cf_x86_phone-userdebug"
561            kernel_build_id: String of the kernel image build id.
562            kernel_branch: String of the kernel image branch name.
563            kernel_build_target: String of the kernel image target name,
564            bootloader_build_id: String of the bootloader build id.
565            bootloader_branch: String of the bootloader branch name.
566            bootloader_build_target: String of the bootloader target name.
567            ota_build_id: String of the otatools build id.
568            ota_branch: String of the otatools branch name.
569            ota_build_target: String of the otatools target name.
570
571        Returns:
572            List of string args for fetch_cvd.
573        """
574        timestart = time.time()
575        fetch_cvd_args = ["-credential_source=gce"]
576        fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
577            build_id, branch, build_target, system_build_id, system_branch,
578            system_build_target, kernel_build_id, kernel_branch,
579            kernel_build_target, bootloader_build_id, bootloader_branch,
580            bootloader_build_target, ota_build_id, ota_branch, ota_build_target)
581        fetch_cvd_args.extend(fetch_cvd_build_args)
582
583        self._ssh.Run("./fetch_cvd " + " ".join(fetch_cvd_args),
584                      timeout=constants.DEFAULT_SSH_TIMEOUT)
585        self._execution_time[_FETCH_ARTIFACT] = round(time.time() - timestart, 2)
586
587    @utils.TimeExecute(function_description="Update instance's certificates")
588    def UpdateCertificate(self):
589        """Update webrtc default certificates of the remote instance.
590
591        For trusting both the localhost and remote instance, the process will
592        upload certificates(rootCA.pem, server.crt, server.key) and the mkcert
593        tool from the client workstation to remote instance where running the
594        mkcert with the uploaded rootCA file and replace the webrtc frontend
595        default certificates for connecting to a remote webrtc AVD without the
596        insecure warning.
597        """
598        local_cert_dir = os.path.join(os.path.expanduser("~"),
599                                      constants.SSL_DIR)
600        if mkcert.AllocateLocalHostCert():
601            upload_files = []
602            for cert_file in (constants.WEBRTC_CERTS_FILES +
603                              [f"{constants.SSL_CA_NAME}.pem"]):
604                upload_files.append(os.path.join(local_cert_dir,
605                                                 cert_file))
606            try:
607                self._ssh.ScpPushFiles(upload_files, constants.WEBRTC_CERTS_PATH)
608                self._ssh.Run(_TRUST_REMOTE_INSTANCE_COMMAND)
609            except subprocess.CalledProcessError:
610                logger.debug("Update WebRTC frontend certificate failed.")
611
612    @utils.TimeExecute(function_description="Upload extra files to instance")
613    def UploadExtraFiles(self, extra_files):
614        """Upload extra files into GCE instance.
615
616        Args:
617            extra_files: List of namedtuple ExtraFile.
618
619        Raises:
620            errors.CheckPathError: The provided path doesn't exist.
621        """
622        for extra_file in extra_files:
623            if not os.path.exists(extra_file.source):
624                raise errors.CheckPathError(
625                    "The path doesn't exist: %s" % extra_file.source)
626            self._ssh.ScpPushFile(extra_file.source, extra_file.target)
627
628    def GetSshConnectCmd(self):
629        """Get ssh connect command.
630
631        Returns:
632            String of ssh connect command.
633        """
634        return self._ssh.GetBaseCmd(constants.SSH_BIN)
635
636    def GetInstanceIP(self, instance=None):
637        """Override method from parent class.
638
639        It need to get the IP address in the common_operation. If the class
640        already defind the ip address, return the ip address.
641
642        Args:
643            instance: String, representing instance name.
644
645        Returns:
646            ssh.IP object, that stores internal and external ip of the instance.
647        """
648        if self._ip:
649            return self._ip
650        return gcompute_client.ComputeClient.GetInstanceIP(
651            self, instance=instance, zone=self._zone)
652
653    def GetHostImageName(self, stable_image_name, image_family, image_project):
654        """Get host image name.
655
656        Args:
657            stable_image_name: String of stable host image name.
658            image_family: String of image family.
659            image_project: String of image project.
660
661        Returns:
662            String of stable host image name.
663
664        Raises:
665            errors.ConfigError: There is no host image name in config file.
666        """
667        if stable_image_name:
668            return stable_image_name
669
670        if image_family:
671            image_name = gcompute_client.ComputeClient.GetImageFromFamily(
672                self, image_family, image_project)["name"]
673            logger.debug("Get the host image name from image family: %s", image_name)
674            return image_name
675
676        raise errors.ConfigError(
677            "Please specify 'stable_host_image_name' or 'stable_host_image_family'"
678            " in config.")
679
680    def SetStage(self, stage):
681        """Set stage to know the create progress.
682
683        Args:
684            stage: Integer, the stage would like STAGE_INIT, STAGE_GCE.
685        """
686        self._stage = stage
687
688    def _UpdateOpenWrtStatus(self, avd_spec):
689        """Update the OpenWrt device status.
690
691        Args:
692            avd_spec: An AVDSpec instance.
693        """
694        self._openwrt = avd_spec.openwrt if avd_spec else False
695
696    @property
697    def all_failures(self):
698        """Return all_failures"""
699        return self._all_failures
700
701    @property
702    def execution_time(self):
703        """Return execution_time"""
704        return self._execution_time
705
706    @property
707    def stage(self):
708        """Return stage"""
709        return self._stage
710
711    @property
712    def openwrt(self):
713        """Return openwrt"""
714        return self._openwrt
715
716    @property
717    def build_api(self):
718        """Return build_api"""
719        return self._build_api
720