• 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, subprocess.TimeoutExpired) 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, subprocess.TimeoutExpired) 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        # Android boolean parsing does not recognize capitalized True/False as valid
355        lowercase_enable_value = str(self._avd_spec.enable_fetch_local_caching).lower()
356        fetch_cvd_args = self._avd_spec.fetch_cvd_wrapper.split(',') + [
357            f"-fetch_cvd_path={constants.CMD_CVD_FETCH[0]}",
358            constants.CMD_CVD_FETCH[1],
359            f"-target_directory={self._GetArtifactPath()}",
360            self._GetRemoteFetchCredentialArg(),
361            f"-enable_caching={lowercase_enable_value}"]
362        fetch_cvd_args.extend(fetch_cvd_build_args)
363
364        ssh_cmd = self._ssh.GetBaseCmd(constants.SSH_BIN)
365        cmd = (f"{ssh_cmd} -- " + " ".join(fetch_cvd_args))
366        logger.debug("cmd:\n %s", cmd)
367        ssh.ShellCmdWithRetry(cmd)
368
369    @utils.TimeExecute(
370        function_description="Downloading artifacts on remote host")
371    def _DownloadArtifactsRemotehost(self):
372        """Generate fetch_cvd args and run fetch_cvd on remote host to
373        download artifacts.
374        """
375        fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
376            self._avd_spec.remote_image,
377            self._avd_spec.system_build_info,
378            self._avd_spec.kernel_build_info,
379            self._avd_spec.boot_build_info,
380            self._avd_spec.bootloader_build_info,
381            self._avd_spec.android_efi_loader_build_info,
382            self._avd_spec.ota_build_info,
383            self._avd_spec.host_package_build_info)
384
385        fetch_cvd_args = list(constants.CMD_CVD_FETCH)
386        # Android boolean parsing does not recognize capitalized True/False as valid
387        lowercase_enable_value = str(self._avd_spec.enable_fetch_local_caching).lower()
388        fetch_cvd_args.extend([f"-target_directory={self._GetArtifactPath()}",
389                               self._GetRemoteFetchCredentialArg(),
390                               f"-enable_caching={lowercase_enable_value}"])
391        fetch_cvd_args.extend(fetch_cvd_build_args)
392
393        ssh_cmd = self._ssh.GetBaseCmd(constants.SSH_BIN)
394        cmd = (f"{ssh_cmd} -- " + " ".join(fetch_cvd_args))
395        logger.debug("cmd:\n %s", cmd)
396        ssh.ShellCmdWithRetry(cmd)
397
398    @utils.TimeExecute(function_description="Download and upload fetch_cvd")
399    def _UploadFetchCvd(self, extract_path):
400        """Duplicate service account json private key when available and upload
401           to remote host.
402
403        Args:
404            extract_path: String, a path include extracted files.
405        """
406        cfg = self._avd_spec.cfg
407        # Duplicate fetch_cvd API key when available
408        if cfg.service_account_json_private_key_path:
409            shutil.copyfile(
410                cfg.service_account_json_private_key_path,
411                os.path.join(extract_path, constants.FETCH_CVD_CREDENTIAL_SOURCE))
412
413        self._UploadRemoteImageArtifacts(extract_path)
414
415    @utils.TimeExecute(function_description="Downloading Android Build artifact")
416    def _DownloadArtifacts(self, extract_path):
417        """Download the CF image artifacts and process them.
418
419        - Download images from the Android Build system.
420        - Download cvd host package from the Android Build system.
421
422        Args:
423            extract_path: String, a path include extracted files.
424
425        Raises:
426            errors.GetRemoteImageError: Fails to download rom images.
427        """
428        cfg = self._avd_spec.cfg
429
430        # Download images with fetch_cvd
431        fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
432            self._avd_spec.remote_image,
433            self._avd_spec.system_build_info,
434            self._avd_spec.kernel_build_info,
435            self._avd_spec.boot_build_info,
436            self._avd_spec.bootloader_build_info,
437            self._avd_spec.android_efi_loader_build_info,
438            self._avd_spec.ota_build_info,
439            self._avd_spec.host_package_build_info)
440        creds_cache_file = os.path.join(_HOME_FOLDER, cfg.creds_cache_file)
441        fetch_cvd_cert_arg = self._build_api.GetFetchCertArg(creds_cache_file)
442        fetch_cvd_args = list(constants.CMD_CVD_FETCH)
443        # Android boolean parsing does not recognize capitalized True/False as valid
444        lowercase_enable_value = str(self._avd_spec.enable_fetch_local_caching).lower()
445        fetch_cvd_args.extend([f"-target_directory={extract_path}",
446                               fetch_cvd_cert_arg,
447                               f"-enable_caching={lowercase_enable_value}"])
448        fetch_cvd_args.extend(fetch_cvd_build_args)
449        logger.debug("Download images command: %s", fetch_cvd_args)
450        try:
451            subprocess.check_call(fetch_cvd_args)
452        except subprocess.CalledProcessError as e:
453            raise errors.GetRemoteImageError(f"Fails to download images: {e}")
454
455    @utils.TimeExecute(function_description="Uploading remote image artifacts")
456    def _UploadRemoteImageArtifacts(self, images_dir):
457        """Upload remote image artifacts to instance.
458
459        Args:
460            images_dir: String, directory of local artifacts downloaded by
461                        fetch_cvd.
462        """
463        artifact_files = [
464            os.path.basename(image)
465            for image in glob.glob(os.path.join(images_dir, _ALL_FILES))
466        ]
467        ssh_cmd = self._ssh.GetBaseCmd(constants.SSH_BIN)
468        # TODO(b/182259589): Refactor upload image command into a function.
469        cmd = (f"tar -cf - --lzop -S -C {images_dir} "
470               f"{' '.join(artifact_files)} | "
471               f"{ssh_cmd} -- "
472               f"tar -xf - --lzop -S -C {self._GetArtifactPath()}")
473        logger.debug("cmd:\n %s", cmd)
474        ssh.ShellCmdWithRetry(cmd)
475
476    @staticmethod
477    def _ReplaceRemoteImageArgs(launch_cvd_args, old_dir, new_dir):
478        """Replace the prefix of launch_cvd path arguments.
479
480        Args:
481            launch_cvd_args: A list of string pairs. Each pair consists of a
482                             launch_cvd option and a remote path.
483            old_dir: The prefix of the paths to be replaced.
484            new_dir: The new prefix of the paths.
485
486        Returns:
487            A list of string pairs, the replaced arguments.
488
489        Raises:
490            errors.CreateError if any path cannot be replaced.
491        """
492        if any(remote_path.isabs(path) != remote_path.isabs(old_dir) for
493               _, path in launch_cvd_args):
494            raise errors.CreateError(f"Cannot convert {launch_cvd_args} to "
495                                     f"relative paths under {old_dir}")
496        return [(option,
497                 remote_path.join(new_dir, remote_path.relpath(path, old_dir)))
498                for option, path in launch_cvd_args]
499
500    @utils.TimeExecute(function_description="Copying images")
501    def _CopyRemoteImageDir(self, remote_src_dir, remote_dst_dir):
502        """Copy a remote directory recursively.
503
504        Args:
505            remote_src_dir: The source directory.
506            remote_dst_dir: The destination directory.
507        """
508        self._ssh.Run(f"cp -frT {remote_src_dir} {remote_dst_dir}")
509
510    @utils.TimeExecute(
511        function_description="Launching AVD(s) and waiting for boot up",
512        result_evaluator=utils.BootEvaluator)
513    def _LaunchCvd(self, image_args, deadline):
514        """Execute launch_cvd.
515
516        Args:
517            image_args: A list of strings, the extra arguments generated by
518                        acloud for remote image paths.
519            deadline: The timestamp when the timeout expires.
520
521        Returns:
522            The error message as a string. An empty string represents success.
523        """
524        config = cvd_utils.GetConfigFromRemoteAndroidInfo(
525            self._ssh, self._GetArtifactPath())
526        cmd = cvd_utils.GetRemoteLaunchCvdCmd(
527            self._GetInstancePath(), self._avd_spec, config, image_args)
528        boot_timeout_secs = deadline - time.time()
529        if boot_timeout_secs <= 0:
530            return "Timed out before launch_cvd."
531
532        self._compute_client.ExtendReportData(
533            constants.LAUNCH_CVD_COMMAND, cmd)
534        error_msg = cvd_utils.ExecuteRemoteLaunchCvd(
535            self._ssh, cmd, boot_timeout_secs)
536        self._compute_client.openwrt = not error_msg and self._avd_spec.openwrt
537        return error_msg
538
539    def _FindLogFiles(self, instance, download):
540        """Find and pull all log files from instance.
541
542        Args:
543            instance: String, instance name.
544            download: Whether to download the files to a temporary directory
545                      and show messages to the user.
546        """
547        logs = []
548        if (self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE and
549                self._avd_spec.remote_fetch):
550            logs.append(
551                cvd_utils.GetRemoteFetcherConfigJson(self._GetArtifactPath()))
552        logs.extend(cvd_utils.FindRemoteLogs(
553            self._ssh,
554            self._GetInstancePath(),
555            self._avd_spec.base_instance_num,
556            self._avd_spec.num_avds_per_instance))
557        self._all_logs[instance] = logs
558
559        if download:
560            # To avoid long download time, fetch from the first device only.
561            log_files = pull.GetAllLogFilePaths(
562                self._ssh, self._GetInstancePath(constants.REMOTE_LOG_FOLDER))
563            error_log_folder = pull.PullLogs(self._ssh, log_files, instance)
564            self._compute_client.ExtendReportData(constants.ERROR_LOG_FOLDER,
565                                                  error_log_folder)
566
567    def GetOpenWrtInfoDict(self):
568        """Get openwrt info dictionary.
569
570        Returns:
571            A openwrt info dictionary. None for the case is not openwrt device.
572        """
573        if not self._avd_spec.openwrt:
574            return None
575        return cvd_utils.GetOpenWrtInfoDict(self._ssh, self._GetInstancePath())
576
577    def GetBuildInfoDict(self):
578        """Get build info dictionary.
579
580        Returns:
581            A build info dictionary. None for local image case.
582        """
583        if self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL:
584            return None
585        return cvd_utils.GetRemoteBuildInfoDict(self._avd_spec)
586
587    def GetAdbPorts(self):
588        """Get ADB ports of the created devices.
589
590        Returns:
591            The port numbers as a list of integers.
592        """
593        return cvd_utils.GetAdbPorts(self._avd_spec.base_instance_num,
594                                     self._avd_spec.num_avds_per_instance)
595
596    def GetVncPorts(self):
597        """Get VNC ports of the created devices.
598
599        Returns:
600            The port numbers as a list of integers.
601        """
602        return cvd_utils.GetVncPorts(self._avd_spec.base_instance_num,
603                                     self._avd_spec.num_avds_per_instance)
604
605    def GetFailures(self):
606        """Get failures from all devices.
607
608        Returns:
609            A dictionary that contains all the failures.
610            The key is the name of the instance that fails to boot,
611            and the value is a string or an errors.DeviceBootError object.
612        """
613        return self._all_failures
614
615    def GetLogs(self):
616        """Get all device logs.
617
618        Returns:
619            A dictionary that maps instance names to lists of report.LogFile.
620        """
621        return self._all_logs
622
623    def GetFetchCvdWrapperLogIfExist(self):
624        """Get FetchCvdWrapper log if exist.
625
626        Returns:
627            A dictionary that includes FetchCvdWrapper logs.
628        """
629        if not self._avd_spec.fetch_cvd_wrapper:
630            return {}
631        path = os.path.join(self._GetArtifactPath(), "fetch_cvd_wrapper_log.json")
632        ssh_cmd = self._ssh.GetBaseCmd(constants.SSH_BIN) + " cat " + path
633        proc = subprocess.run(ssh_cmd, shell=True, capture_output=True,
634                              check=False)
635        if proc.stderr:
636            logger.debug("`%s` stderr: %s", ssh_cmd, proc.stderr.decode())
637        if proc.stdout:
638            try:
639                return json.loads(proc.stdout)
640            except ValueError as e:
641                return {"status": "FETCH_WRAPPER_REPORT_PARSE_ERROR"}
642        return {}
643