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