xref: /aosp_15_r20/tools/acloud/public/actions/remote_host_cf_device_factory.py (revision 800a58d989c669b8eb8a71d8df53b1ba3d411444)
1# Copyright 2022 - 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
15"""RemoteHostDeviceFactory implements the device factory interface and creates
16cuttlefish instances on a remote host."""
17
18import glob
19import json
20import logging
21import os
22import posixpath as remote_path
23import shutil
24import subprocess
25import tempfile
26import time
27
28from acloud import errors
29from acloud.internal import constants
30from acloud.internal.lib import auth
31from acloud.internal.lib import android_build_client
32from acloud.internal.lib import cvd_utils
33from acloud.internal.lib import remote_host_client
34from acloud.internal.lib import utils
35from acloud.internal.lib import ssh
36from acloud.public.actions import base_device_factory
37from acloud.pull import pull
38
39
40logger = logging.getLogger(__name__)
41_ALL_FILES = "*"
42_HOME_FOLDER = os.path.expanduser("~")
43_TEMP_PREFIX = "acloud_remote_host"
44_IMAGE_TIMESTAMP_FILE_NAME = "acloud_image_timestamp.txt"
45_IMAGE_ARGS_FILE_NAME = "acloud_image_args.txt"
46
47
48class RemoteHostDeviceFactory(base_device_factory.BaseDeviceFactory):
49    """A class that can produce a cuttlefish device.
50
51    Attributes:
52        avd_spec: AVDSpec object that tells us what we're going to create.
53        local_image_artifact: A string, path to local image.
54        cvd_host_package_artifact: A string, path to cvd host package.
55        all_failures: A dictionary mapping instance names to errors.
56        all_logs: A dictionary mapping instance names to lists of
57                  report.LogFile.
58        compute_client: An object of remote_host_client.RemoteHostClient.
59        ssh: An Ssh object.
60        android_build_client: An android_build_client.AndroidBuildClient that
61                              is lazily initialized.
62    """
63
64    _USER_BUILD = "userbuild"
65
66    def __init__(self, avd_spec, local_image_artifact=None,
67                 cvd_host_package_artifact=None):
68        """Initialize attributes."""
69        self._avd_spec = avd_spec
70        self._local_image_artifact = local_image_artifact
71        self._cvd_host_package_artifact = cvd_host_package_artifact
72        self._all_failures = {}
73        self._all_logs = {}
74        super().__init__(
75            remote_host_client.RemoteHostClient(avd_spec.remote_host))
76        self._ssh = None
77        self._android_build_client = None
78
79    @property
80    def _build_api(self):
81        """Return an android_build_client.AndroidBuildClient object."""
82        if not self._android_build_client:
83            credentials = auth.CreateCredentials(self._avd_spec.cfg)
84            self._android_build_client = android_build_client.AndroidBuildClient(
85                credentials)
86        return self._android_build_client
87
88    def CreateInstance(self):
89        """Create a single configured cuttlefish device.
90
91        Returns:
92            A string, representing instance name.
93        """
94        start_time = time.time()
95        self._compute_client.SetStage(constants.STAGE_SSH_CONNECT)
96        instance = self._InitRemotehost()
97        start_time = self._compute_client.RecordTime(
98            constants.TIME_GCE, start_time)
99
100        deadline = start_time + (self._avd_spec.boot_timeout_secs or
101                                 constants.DEFAULT_CF_BOOT_TIMEOUT)
102        self._compute_client.SetStage(constants.STAGE_ARTIFACT)
103        try:
104            image_args = self._ProcessRemoteHostArtifacts(deadline)
105        except (errors.CreateError, errors.DriverError,
106                subprocess.CalledProcessError) as e:
107            logger.exception("Fail to prepare artifacts.")
108            self._all_failures[instance] = str(e)
109            # If an SSH error or timeout happens, report the name for the
110            # caller to clean up this instance.
111            return instance
112        finally:
113            start_time = self._compute_client.RecordTime(
114                constants.TIME_ARTIFACT, start_time)
115
116        self._compute_client.SetStage(constants.STAGE_BOOT_UP)
117        error_msg = self._LaunchCvd(image_args, deadline)
118        start_time = self._compute_client.RecordTime(
119            constants.TIME_LAUNCH, start_time)
120
121        if error_msg:
122            self._all_failures[instance] = error_msg
123
124        try:
125            self._FindLogFiles(
126                instance, (error_msg and not self._avd_spec.no_pull_log))
127        except (errors.SubprocessFail, errors.DeviceConnectionError,
128                subprocess.CalledProcessError) as e:
129            logger.error("Fail to find log files: %s", e)
130
131        return instance
132
133    def _GetInstancePath(self, relative_path=""):
134        """Append a relative path to the remote base directory.
135
136        Args:
137            relative_path: The remote relative path.
138
139        Returns:
140            The remote base directory if relative_path is empty.
141            The remote path under the base directory otherwise.
142        """
143        base_dir = cvd_utils.GetRemoteHostBaseDir(
144            self._avd_spec.base_instance_num)
145        return (remote_path.join(base_dir, relative_path) if relative_path else
146                base_dir)
147
148    def _GetArtifactPath(self, relative_path=""):
149        """Append a relative path to the remote image directory.
150
151        Args:
152            relative_path: The remote relative path.
153
154        Returns:
155            GetInstancePath if avd_spec.remote_image_dir is empty.
156            avd_spec.remote_image_dir if relative_path is empty.
157            The remote path under avd_spec.remote_image_dir otherwise.
158        """
159        remote_image_dir = self._avd_spec.remote_image_dir
160        if remote_image_dir:
161            return (remote_path.join(remote_image_dir, relative_path)
162                    if relative_path else remote_image_dir)
163        return self._GetInstancePath(relative_path)
164
165    def _InitRemotehost(self):
166        """Determine the remote host instance name and activate ssh.
167
168        Returns:
169            A string, representing instance name.
170        """
171        # Get product name from the img zip file name or TARGET_PRODUCT.
172        image_name = os.path.basename(
173            self._local_image_artifact) if self._local_image_artifact else ""
174        build_target = (os.environ.get(constants.ENV_BUILD_TARGET)
175                        if "-" not in image_name else
176                        image_name.split("-", maxsplit=1)[0])
177        build_id = self._USER_BUILD
178        if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE:
179            build_id = self._avd_spec.remote_image[constants.BUILD_ID]
180
181        instance = cvd_utils.FormatRemoteHostInstanceName(
182            self._avd_spec.remote_host, self._avd_spec.base_instance_num,
183            build_id, build_target)
184        ip = ssh.IP(ip=self._avd_spec.remote_host)
185        self._ssh = ssh.Ssh(
186            ip=ip,
187            user=self._avd_spec.host_user,
188            ssh_private_key_path=(self._avd_spec.host_ssh_private_key_path or
189                                  self._avd_spec.cfg.ssh_private_key_path),
190            extra_args_ssh_tunnel=self._avd_spec.cfg.extra_args_ssh_tunnel,
191            report_internal_ip=self._avd_spec.report_internal_ip)
192        self._ssh.WaitForSsh(timeout=self._avd_spec.ins_timeout_secs)
193        cvd_utils.CleanUpRemoteCvd(self._ssh, self._GetInstancePath(),
194                                   raise_error=False)
195        return instance
196
197    def _ProcessRemoteHostArtifacts(self, deadline):
198        """Initialize or reuse the images on the remote host.
199
200        Args:
201            deadline: The timestamp when the timeout expires.
202
203        Returns:
204            A list of strings, the launch_cvd arguments.
205        """
206        remote_image_dir = self._avd_spec.remote_image_dir
207        reuse_remote_image_dir = False
208        if remote_image_dir:
209            remote_args_path = remote_path.join(remote_image_dir,
210                                                _IMAGE_ARGS_FILE_NAME)
211            cvd_utils.PrepareRemoteImageDirLink(
212                self._ssh, self._GetInstancePath(), remote_image_dir)
213            launch_cvd_args = cvd_utils.LoadRemoteImageArgs(
214                self._ssh,
215                remote_path.join(remote_image_dir, _IMAGE_TIMESTAMP_FILE_NAME),
216                remote_args_path, deadline)
217            if launch_cvd_args is not None:
218                logger.info("Reuse the images in %s", remote_image_dir)
219                reuse_remote_image_dir = True
220            logger.info("Create images in %s", remote_image_dir)
221
222        if not reuse_remote_image_dir:
223            launch_cvd_args = self._InitRemoteImageDir()
224
225        if remote_image_dir:
226            if not reuse_remote_image_dir:
227                cvd_utils.SaveRemoteImageArgs(self._ssh, remote_args_path,
228                                              launch_cvd_args)
229            # FIXME: Use the images in remote_image_dir when cuttlefish can
230            # reliably share images.
231            launch_cvd_args = self._ReplaceRemoteImageArgs(
232                launch_cvd_args, remote_image_dir, self._GetInstancePath())
233            self._CopyRemoteImageDir(remote_image_dir, self._GetInstancePath())
234
235        return [arg for arg_pair in launch_cvd_args for arg in arg_pair]
236
237    def _InitRemoteImageDir(self):
238        """Create remote host artifacts.
239
240        - If images source is local, tool will upload images from local site to
241          remote host.
242        - If images source is remote, tool will download images from android
243          build to local and unzip it then upload to remote host, because there
244          is no permission to fetch build rom on the remote host.
245
246        Returns:
247            A list of string pairs, the launch_cvd arguments generated by
248            UploadExtraImages.
249        """
250        self._ssh.Run(f"mkdir -p {self._GetArtifactPath()}")
251
252        launch_cvd_args = []
253        temp_dir = None
254        try:
255            target_files_dir = None
256            if cvd_utils.AreTargetFilesRequired(self._avd_spec):
257                if self._avd_spec.image_source != constants.IMAGE_SRC_LOCAL:
258                    temp_dir = tempfile.mkdtemp(prefix=_TEMP_PREFIX)
259                    self._DownloadTargetFiles(temp_dir)
260                    target_files_dir = temp_dir
261                elif self._local_image_artifact:
262                    temp_dir = tempfile.mkdtemp(prefix=_TEMP_PREFIX)
263                    cvd_utils.ExtractTargetFilesZip(self._local_image_artifact,
264                                                    temp_dir)
265                    target_files_dir = temp_dir
266                else:
267                    target_files_dir = self._avd_spec.local_image_dir
268
269            if self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL:
270                cvd_utils.UploadArtifacts(
271                    self._ssh, self._GetArtifactPath(),
272                    (target_files_dir or self._local_image_artifact or
273                     self._avd_spec.local_image_dir),
274                    self._cvd_host_package_artifact)
275            else:
276                temp_dir = tempfile.mkdtemp(prefix=_TEMP_PREFIX)
277                logger.debug("Extracted path of artifacts: %s", temp_dir)
278                if self._avd_spec.remote_fetch:
279                    # TODO: Check fetch cvd wrapper file is valid.
280                    if self._avd_spec.fetch_cvd_wrapper:
281                        self._UploadFetchCvd(temp_dir)
282                        self._DownloadArtifactsByFetchWrapper()
283                    else:
284                        self._UploadFetchCvd(temp_dir)
285                        self._DownloadArtifactsRemotehost()
286                else:
287                    self._DownloadArtifacts(temp_dir)
288                    self._UploadRemoteImageArtifacts(temp_dir)
289
290            launch_cvd_args.extend(
291                cvd_utils.UploadExtraImages(self._ssh, self._GetArtifactPath(),
292                                            self._avd_spec, target_files_dir))
293        finally:
294            if temp_dir:
295                shutil.rmtree(temp_dir)
296
297        return launch_cvd_args
298
299    def _DownloadTargetFiles(self, temp_dir):
300        """Download and extract target files zip.
301
302        Args:
303            temp_dir: The directory where the zip is extracted.
304        """
305        build_target = self._avd_spec.remote_image[constants.BUILD_TARGET]
306        build_id = self._avd_spec.remote_image[constants.BUILD_ID]
307        with tempfile.NamedTemporaryFile(
308                prefix=_TEMP_PREFIX, suffix=".zip") as target_files_zip:
309            self._build_api.DownloadArtifact(
310                build_target, build_id,
311                cvd_utils.GetMixBuildTargetFilename(build_target, build_id),
312                target_files_zip.name)
313            cvd_utils.ExtractTargetFilesZip(target_files_zip.name,
314                                            temp_dir)
315
316    def _GetRemoteFetchCredentialArg(self):
317        """Get the credential source argument for remote fetch_cvd.
318
319        Remote fetch_cvd uses the service account key uploaded by
320        _UploadFetchCvd if it is available. Otherwise, fetch_cvd uses the
321        token extracted from the local credential file.
322
323        Returns:
324            A string, the credential source argument.
325        """
326        cfg = self._avd_spec.cfg
327        if cfg.service_account_json_private_key_path:
328            return "-credential_source=" + self._GetArtifactPath(
329                constants.FETCH_CVD_CREDENTIAL_SOURCE)
330
331        return self._build_api.GetFetchCertArg(
332            os.path.join(_HOME_FOLDER, cfg.creds_cache_file))
333
334    @utils.TimeExecute(
335        function_description="Downloading artifacts on remote host by fetch "
336                             "cvd wrapper.")
337    def _DownloadArtifactsByFetchWrapper(self):
338        """Generate fetch_cvd args and run fetch cvd wrapper on remote host
339        to download artifacts.
340
341        Fetch cvd wrapper will fetch from cluster cached artifacts, and
342        fallback to fetch_cvd if the artifacts not exist.
343        """
344        fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
345            self._avd_spec.remote_image,
346            self._avd_spec.system_build_info,
347            self._avd_spec.kernel_build_info,
348            self._avd_spec.boot_build_info,
349            self._avd_spec.bootloader_build_info,
350            self._avd_spec.android_efi_loader_build_info,
351            self._avd_spec.ota_build_info,
352            self._avd_spec.host_package_build_info)
353
354        fetch_cvd_args = self._avd_spec.fetch_cvd_wrapper.split(',') + [
355            f"-fetch_cvd_path={constants.CMD_CVD_FETCH[0]}",
356            constants.CMD_CVD_FETCH[1],
357            f"-directory={self._GetArtifactPath()}",
358            self._GetRemoteFetchCredentialArg()]
359        fetch_cvd_args.extend(fetch_cvd_build_args)
360
361        ssh_cmd = self._ssh.GetBaseCmd(constants.SSH_BIN)
362        cmd = (f"{ssh_cmd} -- " + " ".join(fetch_cvd_args))
363        logger.debug("cmd:\n %s", cmd)
364        ssh.ShellCmdWithRetry(cmd)
365
366    @utils.TimeExecute(
367        function_description="Downloading artifacts on remote host")
368    def _DownloadArtifactsRemotehost(self):
369        """Generate fetch_cvd args and run fetch_cvd on remote host to
370        download artifacts.
371        """
372        fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
373            self._avd_spec.remote_image,
374            self._avd_spec.system_build_info,
375            self._avd_spec.kernel_build_info,
376            self._avd_spec.boot_build_info,
377            self._avd_spec.bootloader_build_info,
378            self._avd_spec.android_efi_loader_build_info,
379            self._avd_spec.ota_build_info,
380            self._avd_spec.host_package_build_info)
381
382        fetch_cvd_args = list(constants.CMD_CVD_FETCH)
383        fetch_cvd_args.extend([f"-directory={self._GetArtifactPath()}",
384                               self._GetRemoteFetchCredentialArg()])
385        fetch_cvd_args.extend(fetch_cvd_build_args)
386
387        ssh_cmd = self._ssh.GetBaseCmd(constants.SSH_BIN)
388        cmd = (f"{ssh_cmd} -- " + " ".join(fetch_cvd_args))
389        logger.debug("cmd:\n %s", cmd)
390        ssh.ShellCmdWithRetry(cmd)
391
392    @utils.TimeExecute(function_description="Download and upload fetch_cvd")
393    def _UploadFetchCvd(self, extract_path):
394        """Duplicate service account json private key when available and upload
395           to remote host.
396
397        Args:
398            extract_path: String, a path include extracted files.
399        """
400        cfg = self._avd_spec.cfg
401        # Duplicate fetch_cvd API key when available
402        if cfg.service_account_json_private_key_path:
403            shutil.copyfile(
404                cfg.service_account_json_private_key_path,
405                os.path.join(extract_path, constants.FETCH_CVD_CREDENTIAL_SOURCE))
406
407        self._UploadRemoteImageArtifacts(extract_path)
408
409    @utils.TimeExecute(function_description="Downloading Android Build artifact")
410    def _DownloadArtifacts(self, extract_path):
411        """Download the CF image artifacts and process them.
412
413        - Download images from the Android Build system.
414        - Download cvd host package from the Android Build system.
415
416        Args:
417            extract_path: String, a path include extracted files.
418
419        Raises:
420            errors.GetRemoteImageError: Fails to download rom images.
421        """
422        cfg = self._avd_spec.cfg
423
424        # Download images with fetch_cvd
425        fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
426            self._avd_spec.remote_image,
427            self._avd_spec.system_build_info,
428            self._avd_spec.kernel_build_info,
429            self._avd_spec.boot_build_info,
430            self._avd_spec.bootloader_build_info,
431            self._avd_spec.android_efi_loader_build_info,
432            self._avd_spec.ota_build_info,
433            self._avd_spec.host_package_build_info)
434        creds_cache_file = os.path.join(_HOME_FOLDER, cfg.creds_cache_file)
435        fetch_cvd_cert_arg = self._build_api.GetFetchCertArg(creds_cache_file)
436        fetch_cvd_args = list(constants.CMD_CVD_FETCH)
437        fetch_cvd_args.extend([f"-directory={extract_path}", fetch_cvd_cert_arg])
438        fetch_cvd_args.extend(fetch_cvd_build_args)
439        logger.debug("Download images command: %s", fetch_cvd_args)
440        try:
441            subprocess.check_call(fetch_cvd_args)
442        except subprocess.CalledProcessError as e:
443            raise errors.GetRemoteImageError(f"Fails to download images: {e}")
444
445    @utils.TimeExecute(function_description="Uploading remote image artifacts")
446    def _UploadRemoteImageArtifacts(self, images_dir):
447        """Upload remote image artifacts to instance.
448
449        Args:
450            images_dir: String, directory of local artifacts downloaded by
451                        fetch_cvd.
452        """
453        artifact_files = [
454            os.path.basename(image)
455            for image in glob.glob(os.path.join(images_dir, _ALL_FILES))
456        ]
457        ssh_cmd = self._ssh.GetBaseCmd(constants.SSH_BIN)
458        # TODO(b/182259589): Refactor upload image command into a function.
459        cmd = (f"tar -cf - --lzop -S -C {images_dir} "
460               f"{' '.join(artifact_files)} | "
461               f"{ssh_cmd} -- "
462               f"tar -xf - --lzop -S -C {self._GetArtifactPath()}")
463        logger.debug("cmd:\n %s", cmd)
464        ssh.ShellCmdWithRetry(cmd)
465
466    @staticmethod
467    def _ReplaceRemoteImageArgs(launch_cvd_args, old_dir, new_dir):
468        """Replace the prefix of launch_cvd path arguments.
469
470        Args:
471            launch_cvd_args: A list of string pairs. Each pair consists of a
472                             launch_cvd option and a remote path.
473            old_dir: The prefix of the paths to be replaced.
474            new_dir: The new prefix of the paths.
475
476        Returns:
477            A list of string pairs, the replaced arguments.
478
479        Raises:
480            errors.CreateError if any path cannot be replaced.
481        """
482        if any(remote_path.isabs(path) != remote_path.isabs(old_dir) for
483               _, path in launch_cvd_args):
484            raise errors.CreateError(f"Cannot convert {launch_cvd_args} to "
485                                     f"relative paths under {old_dir}")
486        return [(option,
487                 remote_path.join(new_dir, remote_path.relpath(path, old_dir)))
488                for option, path in launch_cvd_args]
489
490    @utils.TimeExecute(function_description="Copying images")
491    def _CopyRemoteImageDir(self, remote_src_dir, remote_dst_dir):
492        """Copy a remote directory recursively.
493
494        Args:
495            remote_src_dir: The source directory.
496            remote_dst_dir: The destination directory.
497        """
498        self._ssh.Run(f"cp -frT {remote_src_dir} {remote_dst_dir}")
499
500    @utils.TimeExecute(
501        function_description="Launching AVD(s) and waiting for boot up",
502        result_evaluator=utils.BootEvaluator)
503    def _LaunchCvd(self, image_args, deadline):
504        """Execute launch_cvd.
505
506        Args:
507            image_args: A list of strings, the extra arguments generated by
508                        acloud for remote image paths.
509            deadline: The timestamp when the timeout expires.
510
511        Returns:
512            The error message as a string. An empty string represents success.
513        """
514        config = cvd_utils.GetConfigFromRemoteAndroidInfo(
515            self._ssh, self._GetArtifactPath())
516        cmd = cvd_utils.GetRemoteLaunchCvdCmd(
517            self._GetInstancePath(), self._avd_spec, config, image_args)
518        boot_timeout_secs = deadline - time.time()
519        if boot_timeout_secs <= 0:
520            return "Timed out before launch_cvd."
521
522        self._compute_client.ExtendReportData(
523            constants.LAUNCH_CVD_COMMAND, cmd)
524        error_msg = cvd_utils.ExecuteRemoteLaunchCvd(
525            self._ssh, cmd, boot_timeout_secs)
526        self._compute_client.openwrt = not error_msg and self._avd_spec.openwrt
527        return error_msg
528
529    def _FindLogFiles(self, instance, download):
530        """Find and pull all log files from instance.
531
532        Args:
533            instance: String, instance name.
534            download: Whether to download the files to a temporary directory
535                      and show messages to the user.
536        """
537        logs = []
538        if (self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE and
539                self._avd_spec.remote_fetch):
540            logs.append(
541                cvd_utils.GetRemoteFetcherConfigJson(self._GetArtifactPath()))
542        logs.extend(cvd_utils.FindRemoteLogs(
543            self._ssh,
544            self._GetInstancePath(),
545            self._avd_spec.base_instance_num,
546            self._avd_spec.num_avds_per_instance))
547        self._all_logs[instance] = logs
548
549        if download:
550            # To avoid long download time, fetch from the first device only.
551            log_files = pull.GetAllLogFilePaths(
552                self._ssh, self._GetInstancePath(constants.REMOTE_LOG_FOLDER))
553            error_log_folder = pull.PullLogs(self._ssh, log_files, instance)
554            self._compute_client.ExtendReportData(constants.ERROR_LOG_FOLDER,
555                                                  error_log_folder)
556
557    def GetOpenWrtInfoDict(self):
558        """Get openwrt info dictionary.
559
560        Returns:
561            A openwrt info dictionary. None for the case is not openwrt device.
562        """
563        if not self._avd_spec.openwrt:
564            return None
565        return cvd_utils.GetOpenWrtInfoDict(self._ssh, self._GetInstancePath())
566
567    def GetBuildInfoDict(self):
568        """Get build info dictionary.
569
570        Returns:
571            A build info dictionary. None for local image case.
572        """
573        if self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL:
574            return None
575        return cvd_utils.GetRemoteBuildInfoDict(self._avd_spec)
576
577    def GetAdbPorts(self):
578        """Get ADB ports of the created devices.
579
580        Returns:
581            The port numbers as a list of integers.
582        """
583        return cvd_utils.GetAdbPorts(self._avd_spec.base_instance_num,
584                                     self._avd_spec.num_avds_per_instance)
585
586    def GetVncPorts(self):
587        """Get VNC ports of the created devices.
588
589        Returns:
590            The port numbers as a list of integers.
591        """
592        return cvd_utils.GetVncPorts(self._avd_spec.base_instance_num,
593                                     self._avd_spec.num_avds_per_instance)
594
595    def GetFailures(self):
596        """Get failures from all devices.
597
598        Returns:
599            A dictionary that contains all the failures.
600            The key is the name of the instance that fails to boot,
601            and the value is a string or an errors.DeviceBootError object.
602        """
603        return self._all_failures
604
605    def GetLogs(self):
606        """Get all device logs.
607
608        Returns:
609            A dictionary that maps instance names to lists of report.LogFile.
610        """
611        return self._all_logs
612
613    def GetFetchCvdWrapperLogIfExist(self):
614        """Get FetchCvdWrapper log if exist.
615
616        Returns:
617            A dictionary that includes FetchCvdWrapper logs.
618        """
619        if not self._avd_spec.fetch_cvd_wrapper:
620            return {}
621        path = os.path.join(self._GetArtifactPath(), "fetch_cvd_wrapper_log.json")
622        ssh_cmd = self._ssh.GetBaseCmd(constants.SSH_BIN) + " cat " + path
623        proc = subprocess.run(ssh_cmd, shell=True, capture_output=True,
624                              check=False)
625        if proc.stderr:
626            logger.debug("`%s` stderr: %s", ssh_cmd, proc.stderr.decode())
627        if proc.stdout:
628            try:
629                return json.loads(proc.stdout)
630            except ValueError as e:
631                return {"status": "FETCH_WRAPPER_REPORT_PARSE_ERROR"}
632        return {}
633