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