• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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        self._FindLogFiles(
124            instance, (error_msg and not self._avd_spec.no_pull_log))
125        return instance
126
127    def _GetInstancePath(self, relative_path=""):
128        """Append a relative path to the remote base directory.
129
130        Args:
131            relative_path: The remote relative path.
132
133        Returns:
134            The remote base directory if relative_path is empty.
135            The remote path under the base directory otherwise.
136        """
137        base_dir = cvd_utils.GetRemoteHostBaseDir(
138            self._avd_spec.base_instance_num)
139        return (remote_path.join(base_dir, relative_path) if relative_path else
140                base_dir)
141
142    def _GetArtifactPath(self, relative_path=""):
143        """Append a relative path to the remote image directory.
144
145        Args:
146            relative_path: The remote relative path.
147
148        Returns:
149            GetInstancePath if avd_spec.remote_image_dir is empty.
150            avd_spec.remote_image_dir if relative_path is empty.
151            The remote path under avd_spec.remote_image_dir otherwise.
152        """
153        remote_image_dir = self._avd_spec.remote_image_dir
154        if remote_image_dir:
155            return (remote_path.join(remote_image_dir, relative_path)
156                    if relative_path else remote_image_dir)
157        return self._GetInstancePath(relative_path)
158
159    def _InitRemotehost(self):
160        """Determine the remote host instance name and activate ssh.
161
162        Returns:
163            A string, representing instance name.
164        """
165        # Get product name from the img zip file name or TARGET_PRODUCT.
166        image_name = os.path.basename(
167            self._local_image_artifact) if self._local_image_artifact else ""
168        build_target = (os.environ.get(constants.ENV_BUILD_TARGET)
169                        if "-" not in image_name else
170                        image_name.split("-", maxsplit=1)[0])
171        build_id = self._USER_BUILD
172        if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE:
173            build_id = self._avd_spec.remote_image[constants.BUILD_ID]
174
175        instance = cvd_utils.FormatRemoteHostInstanceName(
176            self._avd_spec.remote_host, self._avd_spec.base_instance_num,
177            build_id, build_target)
178        ip = ssh.IP(ip=self._avd_spec.remote_host)
179        self._ssh = ssh.Ssh(
180            ip=ip,
181            user=self._avd_spec.host_user,
182            ssh_private_key_path=(self._avd_spec.host_ssh_private_key_path or
183                                  self._avd_spec.cfg.ssh_private_key_path),
184            extra_args_ssh_tunnel=self._avd_spec.cfg.extra_args_ssh_tunnel,
185            report_internal_ip=self._avd_spec.report_internal_ip)
186        self._ssh.WaitForSsh(timeout=self._avd_spec.ins_timeout_secs)
187        cvd_utils.CleanUpRemoteCvd(self._ssh, self._GetInstancePath(),
188                                   raise_error=False)
189        return instance
190
191    def _ProcessRemoteHostArtifacts(self, deadline):
192        """Initialize or reuse the images on the remote host.
193
194        Args:
195            deadline: The timestamp when the timeout expires.
196
197        Returns:
198            A list of strings, the launch_cvd arguments.
199        """
200        remote_image_dir = self._avd_spec.remote_image_dir
201        reuse_remote_image_dir = False
202        if remote_image_dir:
203            remote_args_path = remote_path.join(remote_image_dir,
204                                                _IMAGE_ARGS_FILE_NAME)
205            cvd_utils.PrepareRemoteImageDirLink(
206                self._ssh, self._GetInstancePath(), remote_image_dir)
207            launch_cvd_args = cvd_utils.LoadRemoteImageArgs(
208                self._ssh,
209                remote_path.join(remote_image_dir, _IMAGE_TIMESTAMP_FILE_NAME),
210                remote_args_path, deadline)
211            if launch_cvd_args is not None:
212                logger.info("Reuse the images in %s", remote_image_dir)
213                reuse_remote_image_dir = True
214            logger.info("Create images in %s", remote_image_dir)
215
216        if not reuse_remote_image_dir:
217            launch_cvd_args = self._InitRemoteImageDir()
218
219        if remote_image_dir:
220            if not reuse_remote_image_dir:
221                cvd_utils.SaveRemoteImageArgs(self._ssh, remote_args_path,
222                                              launch_cvd_args)
223            # FIXME: Use the images in remote_image_dir when cuttlefish can
224            # reliably share images.
225            launch_cvd_args = self._ReplaceRemoteImageArgs(
226                launch_cvd_args, remote_image_dir, self._GetInstancePath())
227            self._CopyRemoteImageDir(remote_image_dir, self._GetInstancePath())
228
229        return [arg for arg_pair in launch_cvd_args for arg in arg_pair]
230
231    def _InitRemoteImageDir(self):
232        """Create remote host artifacts.
233
234        - If images source is local, tool will upload images from local site to
235          remote host.
236        - If images source is remote, tool will download images from android
237          build to local and unzip it then upload to remote host, because there
238          is no permission to fetch build rom on the remote host.
239
240        Returns:
241            A list of string pairs, the launch_cvd arguments generated by
242            UploadExtraImages.
243        """
244        self._ssh.Run(f"mkdir -p {self._GetArtifactPath()}")
245
246        launch_cvd_args = []
247        temp_dir = None
248        try:
249            target_files_dir = None
250            if cvd_utils.AreTargetFilesRequired(self._avd_spec):
251                if self._avd_spec.image_source != constants.IMAGE_SRC_LOCAL:
252                    temp_dir = tempfile.mkdtemp(prefix=_TEMP_PREFIX)
253                    self._DownloadTargetFiles(temp_dir)
254                    target_files_dir = temp_dir
255                elif self._local_image_artifact:
256                    temp_dir = tempfile.mkdtemp(prefix=_TEMP_PREFIX)
257                    cvd_utils.ExtractTargetFilesZip(self._local_image_artifact,
258                                                    temp_dir)
259                    target_files_dir = temp_dir
260                else:
261                    target_files_dir = self._avd_spec.local_image_dir
262
263            if self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL:
264                cvd_utils.UploadArtifacts(
265                    self._ssh, self._GetArtifactPath(),
266                    (target_files_dir or self._local_image_artifact or
267                     self._avd_spec.local_image_dir),
268                    self._cvd_host_package_artifact)
269            else:
270                temp_dir = tempfile.mkdtemp(prefix=_TEMP_PREFIX)
271                logger.debug("Extracted path of artifacts: %s", temp_dir)
272                if self._avd_spec.remote_fetch:
273                    # TODO: Check fetch cvd wrapper file is valid.
274                    if self._avd_spec.fetch_cvd_wrapper:
275                        self._UploadFetchCvd(temp_dir)
276                        self._DownloadArtifactsByFetchWrapper()
277                    else:
278                        self._UploadFetchCvd(temp_dir)
279                        self._DownloadArtifactsRemotehost()
280                else:
281                    self._DownloadArtifacts(temp_dir)
282                    self._UploadRemoteImageArtifacts(temp_dir)
283
284            launch_cvd_args.extend(
285                cvd_utils.UploadExtraImages(self._ssh, self._GetArtifactPath(),
286                                            self._avd_spec, target_files_dir))
287        finally:
288            if temp_dir:
289                shutil.rmtree(temp_dir)
290
291        return launch_cvd_args
292
293    def _DownloadTargetFiles(self, temp_dir):
294        """Download and extract target files zip.
295
296        Args:
297            temp_dir: The directory where the zip is extracted.
298        """
299        build_target = self._avd_spec.remote_image[constants.BUILD_TARGET]
300        build_id = self._avd_spec.remote_image[constants.BUILD_ID]
301        with tempfile.NamedTemporaryFile(
302                prefix=_TEMP_PREFIX, suffix=".zip") as target_files_zip:
303            self._build_api.DownloadArtifact(
304                build_target, build_id,
305                cvd_utils.GetMixBuildTargetFilename(build_target, build_id),
306                target_files_zip.name)
307            cvd_utils.ExtractTargetFilesZip(target_files_zip.name,
308                                            temp_dir)
309
310    def _GetRemoteFetchCredentialArg(self):
311        """Get the credential source argument for remote fetch_cvd.
312
313        Remote fetch_cvd uses the service account key uploaded by
314        _UploadFetchCvd if it is available. Otherwise, fetch_cvd uses the
315        token extracted from the local credential file.
316
317        Returns:
318            A string, the credential source argument.
319        """
320        cfg = self._avd_spec.cfg
321        if cfg.service_account_json_private_key_path:
322            return "-credential_source=" + self._GetArtifactPath(
323                constants.FETCH_CVD_CREDENTIAL_SOURCE)
324
325        return self._build_api.GetFetchCertArg(
326            os.path.join(_HOME_FOLDER, cfg.creds_cache_file))
327
328    @utils.TimeExecute(
329        function_description="Downloading artifacts on remote host by fetch "
330                             "cvd wrapper.")
331    def _DownloadArtifactsByFetchWrapper(self):
332        """Generate fetch_cvd args and run fetch cvd wrapper on remote host
333        to download artifacts.
334
335        Fetch cvd wrapper will fetch from cluster cached artifacts, and
336        fallback to fetch_cvd if the artifacts not exist.
337        """
338        fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
339            self._avd_spec.remote_image,
340            self._avd_spec.system_build_info,
341            self._avd_spec.kernel_build_info,
342            self._avd_spec.boot_build_info,
343            self._avd_spec.bootloader_build_info,
344            self._avd_spec.android_efi_loader_build_info,
345            self._avd_spec.ota_build_info,
346            self._avd_spec.host_package_build_info)
347
348        fetch_cvd_args = self._avd_spec.fetch_cvd_wrapper.split(',') + [
349            f"-directory={self._GetArtifactPath()}",
350            f"-fetch_cvd_path={self._GetArtifactPath(constants.FETCH_CVD)}",
351            self._GetRemoteFetchCredentialArg()]
352        fetch_cvd_args.extend(fetch_cvd_build_args)
353
354        ssh_cmd = self._ssh.GetBaseCmd(constants.SSH_BIN)
355        cmd = (f"{ssh_cmd} -- " + " ".join(fetch_cvd_args))
356        logger.debug("cmd:\n %s", cmd)
357        ssh.ShellCmdWithRetry(cmd)
358
359    @utils.TimeExecute(
360        function_description="Downloading artifacts on remote host")
361    def _DownloadArtifactsRemotehost(self):
362        """Generate fetch_cvd args and run fetch_cvd on remote host to
363        download artifacts.
364        """
365        fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
366            self._avd_spec.remote_image,
367            self._avd_spec.system_build_info,
368            self._avd_spec.kernel_build_info,
369            self._avd_spec.boot_build_info,
370            self._avd_spec.bootloader_build_info,
371            self._avd_spec.android_efi_loader_build_info,
372            self._avd_spec.ota_build_info,
373            self._avd_spec.host_package_build_info)
374
375        fetch_cvd_args = [self._GetArtifactPath(constants.FETCH_CVD),
376                          f"-directory={self._GetArtifactPath()}",
377                          self._GetRemoteFetchCredentialArg()]
378        fetch_cvd_args.extend(fetch_cvd_build_args)
379
380        ssh_cmd = self._ssh.GetBaseCmd(constants.SSH_BIN)
381        cmd = (f"{ssh_cmd} -- " + " ".join(fetch_cvd_args))
382        logger.debug("cmd:\n %s", cmd)
383        ssh.ShellCmdWithRetry(cmd)
384
385    @utils.TimeExecute(function_description="Download and upload fetch_cvd")
386    def _UploadFetchCvd(self, extract_path):
387        """Download fetch_cvd, duplicate service account json private key when available and upload
388           to remote host.
389
390        Args:
391            extract_path: String, a path include extracted files.
392        """
393        cfg = self._avd_spec.cfg
394        is_arm_flavor = cvd_utils.RunOnArmMachine(self._ssh) and self._avd_spec.remote_fetch
395        fetch_cvd = os.path.join(extract_path, constants.FETCH_CVD)
396        self._build_api.DownloadFetchcvd(
397            fetch_cvd, self._avd_spec.fetch_cvd_version, is_arm_flavor)
398        # Duplicate fetch_cvd API key when available
399        if cfg.service_account_json_private_key_path:
400            shutil.copyfile(
401                cfg.service_account_json_private_key_path,
402                os.path.join(extract_path, constants.FETCH_CVD_CREDENTIAL_SOURCE))
403
404        self._UploadRemoteImageArtifacts(extract_path)
405
406    @utils.TimeExecute(function_description="Downloading Android Build artifact")
407    def _DownloadArtifacts(self, extract_path):
408        """Download the CF image artifacts and process them.
409
410        - Download images from the Android Build system.
411        - Download cvd host package from the Android Build system.
412
413        Args:
414            extract_path: String, a path include extracted files.
415
416        Raises:
417            errors.GetRemoteImageError: Fails to download rom images.
418        """
419        cfg = self._avd_spec.cfg
420
421        # Download images with fetch_cvd
422        fetch_cvd = os.path.join(extract_path, constants.FETCH_CVD)
423        self._build_api.DownloadFetchcvd(
424            fetch_cvd, self._avd_spec.fetch_cvd_version)
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 = [fetch_cvd, f"-directory={extract_path}",
437                          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