• 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 gcompute_client
49from acloud.internal.lib import utils
50from acloud.internal.lib.ssh import Ssh
51from acloud.pull import pull
52
53
54logger = logging.getLogger(__name__)
55
56_CONFIG_ARG = "-config"
57_DECOMPRESS_KERNEL_ARG = "-decompress_kernel=true"
58_AGREEMENT_PROMPT_ARG = "-report_anonymous_usage_stats=y"
59_UNDEFOK_ARG = "-undefok=report_anonymous_usage_stats,config"
60_NUM_AVDS_ARG = "-num_instances=%(num_AVD)s"
61_DEFAULT_BRANCH = "aosp-master"
62_FETCHER_BUILD_TARGET = "aosp_cf_x86_phone-userdebug"
63_FETCHER_NAME = "fetch_cvd"
64# Time info to write in report.
65_FETCH_ARTIFACT = "fetch_artifact_time"
66_GCE_CREATE = "gce_create_time"
67_LAUNCH_CVD = "launch_cvd_time"
68# WebRTC args for launching AVD
69_GUEST_ENFORCE_SECURITY_FALSE = "--guest_enforce_security=false"
70_START_WEBRTC = "--start_webrtc"
71_WEBRTC_ID = "--webrtc_device_id=%(instance)s"
72_VM_MANAGER = "--vm_manager=crosvm"
73_WEBRTC_ARGS = [_GUEST_ENFORCE_SECURITY_FALSE, _START_WEBRTC, _VM_MANAGER]
74_VNC_ARGS = ["--start_vnc_server=true"]
75_NO_RETRY = 0
76# Launch cvd command for acloud report
77_LAUNCH_CVD_COMMAND = "launch_cvd_command"
78
79
80class CvdComputeClient(android_compute_client.AndroidComputeClient):
81    """Client that manages Android Virtual Device."""
82
83    DATA_POLICY_CREATE_IF_MISSING = "create_if_missing"
84    # Data policy to customize disk size.
85    DATA_POLICY_ALWAYS_CREATE = "always_create"
86
87    def __init__(self,
88                 acloud_config,
89                 oauth2_credentials,
90                 boot_timeout_secs=None,
91                 ins_timeout_secs=None,
92                 report_internal_ip=None,
93                 gpu=None):
94        """Initialize.
95
96        Args:
97            acloud_config: An AcloudConfig object.
98            oauth2_credentials: An oauth2client.OAuth2Credentials instance.
99            boot_timeout_secs: Integer, the maximum time to wait for the AVD
100                               to boot up.
101            ins_timeout_secs: Integer, the maximum time to wait for the
102                              instance ready.
103            report_internal_ip: Boolean to report the internal ip instead of
104                                external ip.
105            gpu: String, GPU to attach to the device.
106        """
107        super().__init__(acloud_config, oauth2_credentials)
108
109        self._fetch_cvd_version = acloud_config.fetch_cvd_version
110        self._build_api = (
111            android_build_client.AndroidBuildClient(oauth2_credentials))
112        self._ssh_private_key_path = acloud_config.ssh_private_key_path
113        self._boot_timeout_secs = boot_timeout_secs
114        self._ins_timeout_secs = ins_timeout_secs
115        self._report_internal_ip = report_internal_ip
116        self._gpu = gpu
117        # Store all failures result when creating one or multiple instances.
118        self._all_failures = dict()
119        self._extra_args_ssh_tunnel = acloud_config.extra_args_ssh_tunnel
120        self._ssh = None
121        self._ip = None
122        self._user = constants.GCE_USER
123        self._stage = constants.STAGE_INIT
124        self._execution_time = {_FETCH_ARTIFACT: 0, _GCE_CREATE: 0, _LAUNCH_CVD: 0}
125
126    def InitRemoteHost(self, ssh, ip, user):
127        """Init remote host.
128
129        Check if we can ssh to the remote host, stop any cf instances running
130        on it, and remove existing files.
131
132        Args:
133            ssh: Ssh object.
134            ip: namedtuple (internal, external) that holds IP address of the
135                remote host, e.g. "external:140.110.20.1, internal:10.0.0.1"
136            user: String of user log in to the instance.
137        """
138        self.SetStage(constants.STAGE_SSH_CONNECT)
139        self._ssh = ssh
140        self._ip = ip
141        self._user = user
142        self._ssh.WaitForSsh(timeout=self._ins_timeout_secs)
143        self.StopCvd()
144        self.CleanUp()
145
146    # TODO(171376263): Refactor CreateInstance() args with avd_spec.
147    # pylint: disable=arguments-differ,too-many-locals,broad-except
148    def CreateInstance(self, instance, image_name, image_project,
149                       build_target=None, branch=None, build_id=None,
150                       kernel_branch=None, kernel_build_id=None,
151                       kernel_build_target=None, blank_data_disk_size_gb=None,
152                       avd_spec=None, extra_scopes=None,
153                       system_build_target=None, system_branch=None,
154                       system_build_id=None, bootloader_build_target=None,
155                       bootloader_branch=None, bootloader_build_id=None):
156
157        """Create/Reuse a single configured cuttlefish device.
158        1. Prepare GCE instance.
159           Create a new instnace or get IP address for reusing the specific instance.
160        2. Put fetch_cvd on the instance.
161        3. Invoke fetch_cvd to fetch and run the instance.
162
163        Args:
164            instance: instance name.
165            image_name: A string, the name of the GCE image.
166            image_project: A string, name of the project where the image lives.
167                           Assume the default project if None.
168            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
169            branch: Branch name, e.g. "aosp-master"
170            build_id: Build id, a string, e.g. "2263051", "P2804227"
171            kernel_branch: Kernel branch name, e.g. "kernel-common-android-4.14"
172            kernel_build_id: Kernel build id, a string, e.g. "223051", "P280427"
173            kernel_build_target: String, Kernel build target name.
174            blank_data_disk_size_gb: Size of the blank data disk in GB.
175            avd_spec: An AVDSpec instance.
176            extra_scopes: A list of extra scopes to be passed to the instance.
177            system_build_target: String of the system image target name,
178                                 e.g. "cf_x86_phone-userdebug"
179            system_branch: String of the system image branch name.
180            system_build_id: String of the system image build id.
181            bootloader_build_target: String of the bootloader target name.
182            bootloader_branch: String of the bootloader branch name.
183            bootloader_build_id: String of the bootloader build id.
184
185        Returns:
186            A string, representing instance name.
187        """
188
189        # A blank data disk would be created on the host. Make sure the size of
190        # the boot disk is large enough to hold it.
191        boot_disk_size_gb = (
192            int(self.GetImage(image_name, image_project)["diskSizeGb"]) +
193            blank_data_disk_size_gb)
194
195        # Record the build info into metadata.
196        self._RecordBuildInfo(avd_spec, build_id, build_target,
197                              system_build_id, system_build_target,
198                              kernel_build_id, kernel_build_target)
199
200        if avd_spec and avd_spec.instance_name_to_reuse:
201            self._ip = self._ReusingGceInstance(avd_spec)
202        else:
203            self._VerifyZoneByQuota()
204            self._ip = self._CreateGceInstance(instance, image_name, image_project,
205                                               extra_scopes, boot_disk_size_gb,
206                                               avd_spec)
207        self._ssh = Ssh(ip=self._ip,
208                        user=constants.GCE_USER,
209                        ssh_private_key_path=self._ssh_private_key_path,
210                        extra_args_ssh_tunnel=self._extra_args_ssh_tunnel,
211                        report_internal_ip=self._report_internal_ip)
212        try:
213            self.SetStage(constants.STAGE_SSH_CONNECT)
214            self._ssh.WaitForSsh(timeout=self._ins_timeout_secs)
215            if avd_spec:
216                if avd_spec.instance_name_to_reuse:
217                    self.StopCvd()
218                    self.CleanUp()
219                return instance
220
221            # TODO: Remove following code after create_cf deprecated.
222            self.UpdateFetchCvd()
223
224            self.FetchBuild(build_id, branch, build_target, system_build_id,
225                            system_branch, system_build_target, kernel_build_id,
226                            kernel_branch, kernel_build_target, bootloader_build_id,
227                            bootloader_branch, bootloader_build_target)
228            self.LaunchCvd(instance,
229                           blank_data_disk_size_gb=blank_data_disk_size_gb,
230                           boot_timeout_secs=self._boot_timeout_secs)
231
232            return instance
233        except Exception as e:
234            self._all_failures[instance] = e
235            return instance
236
237    def _RecordBuildInfo(self, avd_spec, build_id, build_target,
238                         system_build_id, system_build_target,
239                         kernel_build_id, kernel_build_target):
240        """Rocord the build information into metadata.
241
242        The build information includes build id and build target of base image,
243        system image, and kernel image.
244
245        Args:
246            avd_spec: An AVDSpec instance.
247            build_id: String, build id for the base image.
248            build_target: String, target name for the base image,
249                          e.g. "cf_x86_phone-userdebug"
250            system_build_id: String, build id for the system image.
251            system_build_target: String, system build target name,
252                                 e.g. "cf_x86_phone-userdebug"
253            kernel_build_id: String, kernel build id, e.g. "223051", "P280427"
254            kernel_build_target: String, kernel build target name.
255        """
256        if avd_spec and avd_spec.image_source == constants.IMAGE_SRC_REMOTE:
257            build_id = avd_spec.remote_image.get(constants.BUILD_ID)
258            build_target = avd_spec.remote_image.get(constants.BUILD_TARGET)
259            system_build_id = avd_spec.system_build_info.get(constants.BUILD_ID)
260            system_build_target = avd_spec.system_build_info.get(constants.BUILD_TARGET)
261            kernel_build_id = avd_spec.kernel_build_info.get(constants.BUILD_ID)
262            kernel_build_target = avd_spec.kernel_build_info.get(constants.BUILD_TARGET)
263        if build_id and build_target:
264            self._metadata.update({"build_id": build_id})
265            self._metadata.update({"build_target": build_target})
266        if system_build_id and system_build_target:
267            self._metadata.update({"system_build_id": system_build_id})
268            self._metadata.update({"system_build_target": system_build_target})
269        if kernel_build_id and kernel_build_target:
270            self._metadata.update({"kernel_build_id": kernel_build_id})
271            self._metadata.update({"kernel_build_target": kernel_build_target})
272
273    # pylint: disable=too-many-branches
274    def _GetLaunchCvdArgs(self, avd_spec=None, blank_data_disk_size_gb=None,
275                          decompress_kernel=None, instance=None):
276        """Get launch_cvd args.
277
278        Args:
279            avd_spec: An AVDSpec instance.
280            blank_data_disk_size_gb: Size of the blank data disk in GB.
281            decompress_kernel: Boolean, if true decompress the kernel.
282            instance: String, instance name.
283
284        Returns:
285            String, args of launch_cvd.
286        """
287        launch_cvd_args = []
288        if blank_data_disk_size_gb and blank_data_disk_size_gb > 0:
289            # Policy 'create_if_missing' would create a blank userdata disk if
290            # missing. If already exist, reuse the disk.
291            launch_cvd_args.append(
292                "-data_policy=" + self.DATA_POLICY_CREATE_IF_MISSING)
293            launch_cvd_args.append(
294                "-blank_data_image_mb=%d" % (blank_data_disk_size_gb * 1024))
295        if avd_spec:
296            launch_cvd_args.append("-config=%s" % avd_spec.flavor)
297            if avd_spec.hw_customize or not self._ArgSupportInLaunchCVD(_CONFIG_ARG):
298                launch_cvd_args.append(
299                    "-x_res=" + avd_spec.hw_property[constants.HW_X_RES])
300                launch_cvd_args.append(
301                    "-y_res=" + avd_spec.hw_property[constants.HW_Y_RES])
302                launch_cvd_args.append(
303                    "-dpi=" + avd_spec.hw_property[constants.HW_ALIAS_DPI])
304                if constants.HW_ALIAS_DISK in avd_spec.hw_property:
305                    launch_cvd_args.append(
306                        "-data_policy=" + self.DATA_POLICY_ALWAYS_CREATE)
307                    launch_cvd_args.append(
308                        "-blank_data_image_mb="
309                        + avd_spec.hw_property[constants.HW_ALIAS_DISK])
310                if constants.HW_ALIAS_CPUS in avd_spec.hw_property:
311                    launch_cvd_args.append(
312                        "-cpus=%s" % avd_spec.hw_property[constants.HW_ALIAS_CPUS])
313                if constants.HW_ALIAS_MEMORY in avd_spec.hw_property:
314                    launch_cvd_args.append(
315                        "-memory_mb=%s" % avd_spec.hw_property[constants.HW_ALIAS_MEMORY])
316            if avd_spec.connect_webrtc:
317                launch_cvd_args.extend(_WEBRTC_ARGS)
318                launch_cvd_args.append(_WEBRTC_ID % {"instance": instance})
319            if avd_spec.connect_vnc:
320                launch_cvd_args.extend(_VNC_ARGS)
321            if avd_spec.num_avds_per_instance > 1:
322                launch_cvd_args.append(
323                    _NUM_AVDS_ARG % {"num_AVD": avd_spec.num_avds_per_instance})
324            if avd_spec.launch_args:
325                launch_cvd_args.append(avd_spec.launch_args)
326        else:
327            resolution = self._resolution.split("x")
328            launch_cvd_args.append("-x_res=" + resolution[0])
329            launch_cvd_args.append("-y_res=" + resolution[1])
330            launch_cvd_args.append("-dpi=" + resolution[3])
331
332        if not avd_spec and self._launch_args:
333            launch_cvd_args.append(self._launch_args)
334
335        if decompress_kernel:
336            launch_cvd_args.append(_DECOMPRESS_KERNEL_ARG)
337
338        launch_cvd_args.append(_UNDEFOK_ARG)
339        launch_cvd_args.append(_AGREEMENT_PROMPT_ARG)
340        return launch_cvd_args
341
342    def _ArgSupportInLaunchCVD(self, arg):
343        """Check if the arg is supported in launch_cvd.
344
345        Args:
346            arg: String of the arg. e.g. "-config".
347
348        Returns:
349            True if this arg is supported. Otherwise False.
350        """
351        if arg in self._ssh.GetCmdOutput("./bin/launch_cvd --help"):
352            return True
353        return False
354
355    def StopCvd(self):
356        """Stop CVD.
357
358        If stop_cvd fails, assume that it's because there was no previously
359        running device.
360        """
361        ssh_command = "./bin/stop_cvd"
362        try:
363            self._ssh.Run(ssh_command)
364        except subprocess.CalledProcessError as e:
365            logger.debug("Failed to stop_cvd (possibly no running device): %s", e)
366
367    def CleanUp(self):
368        """Clean up the files/folders on the existing instance.
369
370        If previous AVD have these files/folders, reusing the instance may have
371        side effects if not cleaned. The path in the instance is /home/vsoc-01/*
372        if the GCE user is vsoc-01.
373        """
374
375        ssh_command = "'/bin/rm -rf /home/%s/*'" % self._user
376        try:
377            self._ssh.Run(ssh_command)
378        except subprocess.CalledProcessError as e:
379            logger.debug("Failed to clean up the files/folders: %s", e)
380
381    @utils.TimeExecute(function_description="Launching AVD(s) and waiting for boot up",
382                       result_evaluator=utils.BootEvaluator)
383    def LaunchCvd(self, instance, avd_spec=None,
384                  blank_data_disk_size_gb=None,
385                  decompress_kernel=None,
386                  boot_timeout_secs=None):
387        """Launch CVD.
388
389        Launch AVD with launch_cvd. If the process is failed, acloud would show
390        error messages and auto download log files from remote instance.
391
392        Args:
393            instance: String, instance name.
394            avd_spec: An AVDSpec instance.
395            blank_data_disk_size_gb: Size of the blank data disk in GB.
396            decompress_kernel: Boolean, if true decompress the kernel.
397            boot_timeout_secs: Integer, the maximum time to wait for the
398                               command to respond.
399
400        Returns:
401           dict of faliures, return this dict for BootEvaluator to handle
402           LaunchCvd success or fail messages.
403        """
404        self.SetStage(constants.STAGE_BOOT_UP)
405        timestart = time.time()
406        error_msg = ""
407        launch_cvd_args = self._GetLaunchCvdArgs(avd_spec,
408                                                 blank_data_disk_size_gb,
409                                                 decompress_kernel,
410                                                 instance)
411        boot_timeout_secs = boot_timeout_secs or constants.DEFAULT_CF_BOOT_TIMEOUT
412        ssh_command = "./bin/launch_cvd -daemon " + " ".join(launch_cvd_args)
413        try:
414            self.ExtendReportData(_LAUNCH_CVD_COMMAND, ssh_command)
415            self._ssh.Run(ssh_command, boot_timeout_secs, retry=_NO_RETRY)
416        except (subprocess.CalledProcessError, errors.DeviceConnectionError) as e:
417            # TODO(b/140475060): Distinguish the error is command return error
418            # or timeout error.
419            error_msg = ("Device %s did not finish on boot within timeout (%s secs)"
420                         % (instance, boot_timeout_secs))
421            self._all_failures[instance] = error_msg
422            utils.PrintColorString(str(e), utils.TextColors.FAIL)
423            if avd_spec and not avd_spec.no_pull_log:
424                self._PullAllLogFiles(instance)
425
426        self._execution_time[_LAUNCH_CVD] = round(time.time() - timestart, 2)
427        return {instance: error_msg} if error_msg else {}
428
429    def _PullAllLogFiles(self, instance):
430        """Pull all log files from instance.
431
432        1. Download log files to temp folder.
433        2. Show messages about the download folder for users.
434
435        Args:
436            instance: String, instance name.
437        """
438        log_files = pull.GetAllLogFilePaths(self._ssh)
439        error_log_folder = pull.GetDownloadLogFolder(instance)
440        pull.PullLogs(self._ssh, log_files, error_log_folder)
441        self.ExtendReportData(constants.ERROR_LOG_FOLDER, error_log_folder)
442
443    @utils.TimeExecute(function_description="Reusing GCE instance")
444    def _ReusingGceInstance(self, avd_spec):
445        """Reusing a cuttlefish existing instance.
446
447        Args:
448            avd_spec: An AVDSpec instance.
449
450        Returns:
451            ssh.IP object, that stores internal and external ip of the instance.
452        """
453        gcompute_client.ComputeClient.AddSshRsaInstanceMetadata(
454            self, constants.GCE_USER, avd_spec.cfg.ssh_public_key_path,
455            avd_spec.instance_name_to_reuse)
456        ip = gcompute_client.ComputeClient.GetInstanceIP(
457            self, instance=avd_spec.instance_name_to_reuse, zone=self._zone)
458
459        return ip
460
461    @utils.TimeExecute(function_description="Creating GCE instance")
462    def _CreateGceInstance(self, instance, image_name, image_project,
463                           extra_scopes, boot_disk_size_gb, avd_spec):
464        """Create a single configured cuttlefish device.
465
466        Override method from parent class.
467        Args:
468            instance: String, instance name.
469            image_name: String, the name of the GCE image.
470            image_project: String, the name of the project where the image.
471            extra_scopes: A list of extra scopes to be passed to the instance.
472            boot_disk_size_gb: Integer, size of the boot disk in GB.
473            avd_spec: An AVDSpec instance.
474
475        Returns:
476            ssh.IP object, that stores internal and external ip of the instance.
477        """
478        self.SetStage(constants.STAGE_GCE)
479        timestart = time.time()
480        metadata = self._metadata.copy()
481
482        if avd_spec:
483            metadata[constants.INS_KEY_AVD_TYPE] = avd_spec.avd_type
484            metadata[constants.INS_KEY_AVD_FLAVOR] = avd_spec.flavor
485            metadata[constants.INS_KEY_DISPLAY] = ("%sx%s (%s)" % (
486                avd_spec.hw_property[constants.HW_X_RES],
487                avd_spec.hw_property[constants.HW_Y_RES],
488                avd_spec.hw_property[constants.HW_ALIAS_DPI]))
489            if avd_spec.gce_metadata:
490                for key, value in avd_spec.gce_metadata.items():
491                    metadata[key] = value
492
493        disk_args = self._GetDiskArgs(
494            instance, image_name, image_project, boot_disk_size_gb)
495        gcompute_client.ComputeClient.CreateInstance(
496            self,
497            instance=instance,
498            image_name=image_name,
499            image_project=image_project,
500            disk_args=disk_args,
501            metadata=metadata,
502            machine_type=self._machine_type,
503            network=self._network,
504            zone=self._zone,
505            gpu=self._gpu,
506            extra_scopes=extra_scopes,
507            tags=["appstreaming"] if (
508                avd_spec and avd_spec.connect_webrtc) else None)
509        ip = gcompute_client.ComputeClient.GetInstanceIP(
510            self, instance=instance, zone=self._zone)
511        logger.debug("'instance_ip': %s", ip.internal
512                     if self._report_internal_ip else ip.external)
513
514        self._execution_time[_GCE_CREATE] = round(time.time() - timestart, 2)
515        return ip
516
517    @utils.TimeExecute(function_description="Uploading build fetcher to instance")
518    def UpdateFetchCvd(self):
519        """Download fetch_cvd from the Build API, and upload it to a remote instance.
520
521        The version of fetch_cvd to use is retrieved from the configuration file. Once fetch_cvd
522        is on the instance, future commands can use it to download relevant Cuttlefish files from
523        the Build API on the instance itself.
524        """
525        self.SetStage(constants.STAGE_ARTIFACT)
526        download_dir = tempfile.mkdtemp()
527        download_target = os.path.join(download_dir, _FETCHER_NAME)
528        self._build_api.DownloadFetchcvd(download_target, self._fetch_cvd_version)
529        self._ssh.ScpPushFile(src_file=download_target, dst_file=_FETCHER_NAME)
530        os.remove(download_target)
531        os.rmdir(download_dir)
532
533    @utils.TimeExecute(function_description="Downloading build on instance")
534    def FetchBuild(self, build_id, branch, build_target, system_build_id,
535                   system_branch, system_build_target, kernel_build_id,
536                   kernel_branch, kernel_build_target, bootloader_build_id,
537                   bootloader_branch, bootloader_build_target):
538        """Execute fetch_cvd on the remote instance to get Cuttlefish runtime files.
539
540        Args:
541            build_id: String of build id, e.g. "2263051", "P2804227"
542            branch: String of branch name, e.g. "aosp-master"
543            build_target: String of target name.
544                          e.g. "aosp_cf_x86_phone-userdebug"
545            system_build_id: String of the system image build id.
546            system_branch: String of the system image branch name.
547            system_build_target: String of the system image target name,
548                                 e.g. "cf_x86_phone-userdebug"
549            kernel_build_id: String of the kernel image build id.
550            kernel_branch: String of the kernel image branch name.
551            kernel_build_target: String of the kernel image target name,
552            bootloader_build_id: String of the bootloader build id.
553            bootloader_branch: String of the bootloader branch name.
554            bootloader_build_target: String of the bootloader target name.
555
556        Returns:
557            List of string args for fetch_cvd.
558        """
559        timestart = time.time()
560        fetch_cvd_args = ["-credential_source=gce"]
561        fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
562            build_id, branch, build_target, system_build_id, system_branch,
563            system_build_target, kernel_build_id, kernel_branch,
564            kernel_build_target, bootloader_build_id, bootloader_branch,
565            bootloader_build_target)
566        fetch_cvd_args.extend(fetch_cvd_build_args)
567
568        self._ssh.Run("./fetch_cvd " + " ".join(fetch_cvd_args),
569                      timeout=constants.DEFAULT_SSH_TIMEOUT)
570        self._execution_time[_FETCH_ARTIFACT] = round(time.time() - timestart, 2)
571
572    def GetInstanceIP(self, instance=None):
573        """Override method from parent class.
574
575        It need to get the IP address in the common_operation. If the class
576        already defind the ip address, return the ip address.
577
578        Args:
579            instance: String, representing instance name.
580
581        Returns:
582            ssh.IP object, that stores internal and external ip of the instance.
583        """
584        if self._ip:
585            return self._ip
586        return gcompute_client.ComputeClient.GetInstanceIP(
587            self, instance=instance, zone=self._zone)
588
589    def GetHostImageName(self, stable_image_name, image_family, image_project):
590        """Get host image name.
591
592        Args:
593            stable_image_name: String of stable host image name.
594            image_family: String of image family.
595            image_project: String of image project.
596
597        Returns:
598            String of stable host image name.
599
600        Raises:
601            errors.ConfigError: There is no host image name in config file.
602        """
603        if stable_image_name:
604            return stable_image_name
605
606        if image_family:
607            image_name = gcompute_client.ComputeClient.GetImageFromFamily(
608                self, image_family, image_project)["name"]
609            logger.debug("Get the host image name from image family: %s", image_name)
610            return image_name
611
612        raise errors.ConfigError(
613            "Please specify 'stable_host_image_name' or 'stable_host_image_family'"
614            " in config.")
615
616    def SetStage(self, stage):
617        """Set stage to know the create progress.
618
619        Args:
620            stage: Integer, the stage would like STAGE_INIT, STAGE_GCE.
621        """
622        self._stage = stage
623
624    @property
625    def all_failures(self):
626        """Return all_failures"""
627        return self._all_failures
628
629    @property
630    def execution_time(self):
631        """Return execution_time"""
632        return self._execution_time
633
634    @property
635    def stage(self):
636        """Return stage"""
637        return self._stage
638
639    @property
640    def build_api(self):
641        """Return build_api"""
642        return self._build_api
643