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