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"""Utility functions that process cuttlefish images.""" 16 17import collections 18import glob 19import logging 20import os 21import posixpath as remote_path 22import re 23import subprocess 24import tempfile 25 26from acloud import errors 27from acloud.create import create_common 28from acloud.internal import constants 29from acloud.internal.lib import ota_tools 30from acloud.internal.lib import ssh 31from acloud.internal.lib import utils 32from acloud.public import report 33 34 35logger = logging.getLogger(__name__) 36 37# Local build artifacts to be uploaded. 38_ARTIFACT_FILES = ["*.img", "bootloader", "kernel"] 39# The boot image name pattern corresponds to the use cases: 40# - In a cuttlefish build environment, ANDROID_PRODUCT_OUT conatins boot.img 41# and boot-debug.img. The former is the default boot image. The latter is not 42# useful for cuttlefish. 43# - In an officially released GKI (Generic Kernel Image) package, the image 44# name is boot-<kernel version>.img. 45_BOOT_IMAGE_NAME_PATTERN = r"boot(-[\d.]+)?\.img" 46_VENDOR_BOOT_IMAGE_NAME = "vendor_boot.img" 47_KERNEL_IMAGE_NAMES = ("kernel", "bzImage", "Image") 48_INITRAMFS_IMAGE_NAME = "initramfs.img" 49_VENDOR_IMAGE_NAMES = ("vendor.img", "vendor_dlkm.img", "odm.img", 50 "odm_dlkm.img") 51VendorImagePaths = collections.namedtuple( 52 "VendorImagePaths", 53 ["vendor", "vendor_dlkm", "odm", "odm_dlkm"]) 54 55# The relative path to the base directory containing cuttelfish images, tools, 56# and runtime files. On a GCE instance, the directory is the SSH user's HOME. 57GCE_BASE_DIR = "." 58_REMOTE_HOST_BASE_DIR_FORMAT = "acloud_cf_%(num)d" 59# Relative paths in a base directory. 60_REMOTE_IMAGE_DIR = "acloud_image" 61_REMOTE_BOOT_IMAGE_PATH = remote_path.join(_REMOTE_IMAGE_DIR, "boot.img") 62_REMOTE_VENDOR_BOOT_IMAGE_PATH = remote_path.join( 63 _REMOTE_IMAGE_DIR, _VENDOR_BOOT_IMAGE_NAME) 64_REMOTE_VBMETA_IMAGE_PATH = remote_path.join(_REMOTE_IMAGE_DIR, "vbmeta.img") 65_REMOTE_KERNEL_IMAGE_PATH = remote_path.join( 66 _REMOTE_IMAGE_DIR, _KERNEL_IMAGE_NAMES[0]) 67_REMOTE_INITRAMFS_IMAGE_PATH = remote_path.join( 68 _REMOTE_IMAGE_DIR, _INITRAMFS_IMAGE_NAME) 69_REMOTE_SUPER_IMAGE_DIR = remote_path.join(_REMOTE_IMAGE_DIR, 70 "super_image_dir") 71 72# Remote host instance name 73_REMOTE_HOST_INSTANCE_NAME_FORMAT = ( 74 constants.INSTANCE_TYPE_HOST + 75 "-%(ip_addr)s-%(num)d-%(build_id)s-%(build_target)s") 76_REMOTE_HOST_INSTANCE_NAME_PATTERN = re.compile( 77 constants.INSTANCE_TYPE_HOST + r"-(?P<ip_addr>[\d.]+)-(?P<num>\d+)-.+") 78# launch_cvd arguments. 79_DATA_POLICY_CREATE_IF_MISSING = "create_if_missing" 80_DATA_POLICY_ALWAYS_CREATE = "always_create" 81_NUM_AVDS_ARG = "-num_instances=%(num_AVD)s" 82AGREEMENT_PROMPT_ARG = "-report_anonymous_usage_stats=y" 83UNDEFOK_ARG = "-undefok=report_anonymous_usage_stats,config" 84# Connect the OpenWrt device via console file. 85_ENABLE_CONSOLE_ARG = "-console=true" 86# WebRTC args 87_WEBRTC_ID = "--webrtc_device_id=%(instance)s" 88_WEBRTC_ARGS = ["--start_webrtc", "--vm_manager=crosvm"] 89_VNC_ARGS = ["--start_vnc_server=true"] 90 91# Cuttlefish runtime directory is specified by `-instance_dir <runtime_dir>`. 92# Cuttlefish tools may create a symbolic link at the specified path. 93# The actual location of the runtime directory depends on the version: 94# 95# In Android 10, the directory is `<runtime_dir>`. 96# 97# In Android 11 and 12, the directory is `<runtime_dir>.<num>`. 98# `<runtime_dir>` is a symbolic link to the first device's directory. 99# 100# In the latest version, if `--instance-dir <runtime_dir>` is specified, the 101# directory is `<runtime_dir>/instances/cvd-<num>`. 102# `<runtime_dir>_runtime` and `<runtime_dir>.<num>` are symbolic links. 103# 104# If `--instance-dir <runtime_dir>` is not specified, the directory is 105# `~/cuttlefish/instances/cvd-<num>`. 106# `~/cuttlefish_runtime` and `~/cuttelfish_runtime.<num>` are symbolic links. 107_LOCAL_LOG_DIR_FORMAT = os.path.join( 108 "%(runtime_dir)s", "instances", "cvd-%(num)d", "logs") 109# Relative paths in a base directory. 110_REMOTE_RUNTIME_DIR_FORMAT = remote_path.join( 111 "cuttlefish", "instances", "cvd-%(num)d") 112_REMOTE_LEGACY_RUNTIME_DIR_FORMAT = "cuttlefish_runtime.%(num)d" 113HOST_KERNEL_LOG = report.LogFile( 114 "/var/log/kern.log", constants.LOG_TYPE_KERNEL_LOG, "host_kernel.log") 115 116# Contents of the target_files archive. 117_DOWNLOAD_MIX_IMAGE_NAME = "{build_target}-target_files-{build_id}.zip" 118_TARGET_FILES_META_DIR_NAME = "META" 119_TARGET_FILES_IMAGES_DIR_NAME = "IMAGES" 120_MISC_INFO_FILE_NAME = "misc_info.txt" 121 122# ARM flavor build target pattern. 123_ARM_TARGET_PATTERN = "arm" 124 125 126def GetAdbPorts(base_instance_num, num_avds_per_instance): 127 """Get ADB ports of cuttlefish. 128 129 Args: 130 base_instance_num: An integer or None, the instance number of the first 131 device. 132 num_avds_per_instance: An integer or None, the number of devices. 133 134 Returns: 135 The port numbers as a list of integers. 136 """ 137 return [constants.CF_ADB_PORT + (base_instance_num or 1) - 1 + index 138 for index in range(num_avds_per_instance or 1)] 139 140def GetFastbootPorts(base_instance_num, num_avds_per_instance): 141 """Get Fastboot ports of cuttlefish. 142 143 Args: 144 base_instance_num: An integer or None, the instance number of the first 145 device. 146 num_avds_per_instance: An integer or None, the number of devices. 147 148 Returns: 149 The port numbers as a list of integers. 150 """ 151 return [constants.CF_FASTBOOT_PORT + (base_instance_num or 1) - 1 + index 152 for index in range(num_avds_per_instance or 1)] 153 154def GetVncPorts(base_instance_num, num_avds_per_instance): 155 """Get VNC ports of cuttlefish. 156 157 Args: 158 base_instance_num: An integer or None, the instance number of the first 159 device. 160 num_avds_per_instance: An integer or None, the number of devices. 161 162 Returns: 163 The port numbers as a list of integers. 164 """ 165 return [constants.CF_VNC_PORT + (base_instance_num or 1) - 1 + index 166 for index in range(num_avds_per_instance or 1)] 167 168 169def _UploadImageZip(ssh_obj, remote_dir, image_zip): 170 """Upload an image zip to a remote host and a GCE instance. 171 172 Args: 173 ssh_obj: An Ssh object. 174 remote_dir: The remote base directory. 175 image_zip: The path to the image zip. 176 """ 177 remote_cmd = f"/usr/bin/install_zip.sh {remote_dir} < {image_zip}" 178 logger.debug("remote_cmd:\n %s", remote_cmd) 179 ssh_obj.Run(remote_cmd) 180 181 182def _UploadImageDir(ssh_obj, remote_dir, image_dir): 183 """Upload an image directory to a remote host or a GCE instance. 184 185 The images are compressed for faster upload. 186 187 Args: 188 ssh_obj: An Ssh object. 189 remote_dir: The remote base directory. 190 image_dir: The directory containing the files to be uploaded. 191 """ 192 try: 193 images_path = os.path.join(image_dir, "required_images") 194 with open(images_path, "r", encoding="utf-8") as images: 195 artifact_files = images.read().splitlines() 196 except IOError: 197 # Older builds may not have a required_images file. In this case 198 # we fall back to *.img. 199 artifact_files = [] 200 for file_name in _ARTIFACT_FILES: 201 artifact_files.extend( 202 os.path.basename(image) for image in glob.glob( 203 os.path.join(image_dir, file_name))) 204 # Upload android-info.txt to parse config value. 205 artifact_files.append(constants.ANDROID_INFO_FILE) 206 cmd = (f"tar -cf - --lzop -S -C {image_dir} {' '.join(artifact_files)} | " 207 f"{ssh_obj.GetBaseCmd(constants.SSH_BIN)} -- " 208 f"tar -xf - --lzop -S -C {remote_dir}") 209 logger.debug("cmd:\n %s", cmd) 210 ssh.ShellCmdWithRetry(cmd) 211 212 213def _UploadCvdHostPackage(ssh_obj, remote_dir, cvd_host_package): 214 """Upload a CVD host package to a remote host or a GCE instance. 215 216 Args: 217 ssh_obj: An Ssh object. 218 remote_dir: The remote base directory. 219 cvd_host_package: The path to the CVD host package. 220 """ 221 if cvd_host_package.endswith(".tar.gz"): 222 remote_cmd = f"tar -xzf - -C {remote_dir} < {cvd_host_package}" 223 logger.debug("remote_cmd:\n %s", remote_cmd) 224 ssh_obj.Run(remote_cmd) 225 else: 226 cmd = (f"tar -cf - --lzop -S -C {cvd_host_package} . | " 227 f"{ssh_obj.GetBaseCmd(constants.SSH_BIN)} -- " 228 f"tar -xf - --lzop -S -C {remote_dir}") 229 logger.debug("cmd:\n %s", cmd) 230 ssh.ShellCmdWithRetry(cmd) 231 232 233@utils.TimeExecute(function_description="Processing and uploading local images") 234def UploadArtifacts(ssh_obj, remote_dir, image_path, cvd_host_package): 235 """Upload images and a CVD host package to a remote host or a GCE instance. 236 237 Args: 238 ssh_obj: An Ssh object. 239 remote_dir: The remote base directory. 240 image_path: A string, the path to the image zip built by `m dist` or 241 the directory containing the images built by `m`. 242 cvd_host_package: A string, the path to the CVD host package in gzip. 243 """ 244 if os.path.isdir(image_path): 245 _UploadImageDir(ssh_obj, remote_dir, image_path) 246 else: 247 _UploadImageZip(ssh_obj, remote_dir, image_path) 248 _UploadCvdHostPackage(ssh_obj, remote_dir, cvd_host_package) 249 250 251def FindBootImages(search_path): 252 """Find boot and vendor_boot images in a path. 253 254 Args: 255 search_path: A path to an image file or an image directory. 256 257 Returns: 258 The boot image path and the vendor_boot image path. Each value can be 259 None if the path doesn't exist. 260 261 Raises: 262 errors.GetLocalImageError if search_path contains more than one boot 263 image or the file format is not correct. 264 """ 265 boot_image_path = create_common.FindBootImage(search_path, 266 raise_error=False) 267 vendor_boot_image_path = os.path.join(search_path, _VENDOR_BOOT_IMAGE_NAME) 268 if not os.path.isfile(vendor_boot_image_path): 269 vendor_boot_image_path = None 270 271 return boot_image_path, vendor_boot_image_path 272 273 274def FindKernelImages(search_path): 275 """Find kernel and initramfs images in a path. 276 277 Args: 278 search_path: A path to an image directory. 279 280 Returns: 281 The kernel image path and the initramfs image path. Each value can be 282 None if the path doesn't exist. 283 """ 284 paths = [os.path.join(search_path, name) for name in _KERNEL_IMAGE_NAMES] 285 kernel_image_path = next((path for path in paths if os.path.isfile(path)), 286 None) 287 288 initramfs_image_path = os.path.join(search_path, _INITRAMFS_IMAGE_NAME) 289 if not os.path.isfile(initramfs_image_path): 290 initramfs_image_path = None 291 292 return kernel_image_path, initramfs_image_path 293 294 295@utils.TimeExecute(function_description="Uploading local kernel images.") 296def _UploadKernelImages(ssh_obj, remote_dir, search_path): 297 """Find and upload kernel or boot images to a remote host or a GCE 298 instance. 299 300 Args: 301 ssh_obj: An Ssh object. 302 remote_dir: The remote base directory. 303 search_path: A path to an image file or an image directory. 304 305 Returns: 306 A list of strings, the launch_cvd arguments including the remote paths. 307 308 Raises: 309 errors.GetLocalImageError if search_path does not contain kernel 310 images. 311 """ 312 # Assume that the caller cleaned up the remote home directory. 313 ssh_obj.Run("mkdir -p " + remote_path.join(remote_dir, _REMOTE_IMAGE_DIR)) 314 315 kernel_image_path, initramfs_image_path = FindKernelImages(search_path) 316 if kernel_image_path and initramfs_image_path: 317 remote_kernel_image_path = remote_path.join( 318 remote_dir, _REMOTE_KERNEL_IMAGE_PATH) 319 remote_initramfs_image_path = remote_path.join( 320 remote_dir, _REMOTE_INITRAMFS_IMAGE_PATH) 321 ssh_obj.ScpPushFile(kernel_image_path, remote_kernel_image_path) 322 ssh_obj.ScpPushFile(initramfs_image_path, remote_initramfs_image_path) 323 return ["-kernel_path", remote_kernel_image_path, 324 "-initramfs_path", remote_initramfs_image_path] 325 326 boot_image_path, vendor_boot_image_path = FindBootImages(search_path) 327 if boot_image_path: 328 remote_boot_image_path = remote_path.join( 329 remote_dir, _REMOTE_BOOT_IMAGE_PATH) 330 ssh_obj.ScpPushFile(boot_image_path, remote_boot_image_path) 331 launch_cvd_args = ["-boot_image", remote_boot_image_path] 332 if vendor_boot_image_path: 333 remote_vendor_boot_image_path = remote_path.join( 334 remote_dir, _REMOTE_VENDOR_BOOT_IMAGE_PATH) 335 ssh_obj.ScpPushFile(vendor_boot_image_path, 336 remote_vendor_boot_image_path) 337 launch_cvd_args.extend(["-vendor_boot_image", 338 remote_vendor_boot_image_path]) 339 return launch_cvd_args 340 341 raise errors.GetLocalImageError( 342 f"{search_path} is not a boot image or a directory containing images.") 343 344 345@utils.TimeExecute(function_description="Uploading disabled vbmeta image.") 346def _UploadDisabledVbmetaImage(ssh_obj, remote_dir, local_tool_dirs): 347 """Upload disabled vbmeta image to a remote host or a GCE instance. 348 349 Args: 350 ssh_obj: An Ssh object. 351 remote_dir: The remote base directory. 352 local_tool_dirs: A list of local directories containing tools. 353 354 Returns: 355 A list of strings, the launch_cvd arguments including the remote paths. 356 357 Raises: 358 CheckPathError if local_tool_dirs do not contain OTA tools. 359 """ 360 # Assume that the caller cleaned up the remote home directory. 361 ssh_obj.Run("mkdir -p " + remote_path.join(remote_dir, _REMOTE_IMAGE_DIR)) 362 363 remote_vbmeta_image_path = remote_path.join(remote_dir, 364 _REMOTE_VBMETA_IMAGE_PATH) 365 with tempfile.NamedTemporaryFile(prefix="vbmeta", 366 suffix=".img") as temp_file: 367 tool_dirs = local_tool_dirs + create_common.GetNonEmptyEnvVars( 368 constants.ENV_ANDROID_SOONG_HOST_OUT, 369 constants.ENV_ANDROID_HOST_OUT) 370 ota = ota_tools.FindOtaTools(tool_dirs) 371 ota.MakeDisabledVbmetaImage(temp_file.name) 372 ssh_obj.ScpPushFile(temp_file.name, remote_vbmeta_image_path) 373 374 return ["-vbmeta_image", remote_vbmeta_image_path] 375 376 377def UploadExtraImages(ssh_obj, remote_dir, avd_spec): 378 """Find and upload the images specified in avd_spec. 379 380 Args: 381 ssh_obj: An Ssh object. 382 remote_dir: The remote base directory. 383 avd_spec: An AvdSpec object containing extra image paths. 384 385 Returns: 386 A list of strings, the launch_cvd arguments including the remote paths. 387 388 Raises: 389 errors.GetLocalImageError if any specified image path does not exist. 390 """ 391 extra_img_args = [] 392 if avd_spec.local_kernel_image: 393 extra_img_args += _UploadKernelImages(ssh_obj, remote_dir, 394 avd_spec.local_kernel_image) 395 if avd_spec.local_vendor_image: 396 extra_img_args += _UploadDisabledVbmetaImage(ssh_obj, remote_dir, 397 avd_spec.local_tool_dirs) 398 return extra_img_args 399 400 401@utils.TimeExecute(function_description="Uploading local super image") 402def UploadSuperImage(ssh_obj, remote_dir, super_image_path): 403 """Upload a super image to a remote host or a GCE instance. 404 405 Args: 406 ssh_obj: An Ssh object. 407 remote_dir: The remote base directory. 408 super_image_path: Path to the super image file. 409 410 Returns: 411 A list of strings, the launch_cvd arguments including the remote paths. 412 """ 413 # Assume that the caller cleaned up the remote home directory. 414 super_image_stem = os.path.basename(super_image_path) 415 remote_super_image_dir = remote_path.join( 416 remote_dir, _REMOTE_SUPER_IMAGE_DIR) 417 remote_super_image_path = remote_path.join( 418 remote_super_image_dir, super_image_stem) 419 ssh_obj.Run(f"mkdir -p {remote_super_image_dir}") 420 cmd = (f"tar -cf - --lzop -S -C {os.path.dirname(super_image_path)} " 421 f"{super_image_stem} | " 422 f"{ssh_obj.GetBaseCmd(constants.SSH_BIN)} -- " 423 f"tar -xf - --lzop -S -C {remote_super_image_dir}") 424 ssh.ShellCmdWithRetry(cmd) 425 launch_cvd_args = ["-super_image", remote_super_image_path] 426 return launch_cvd_args 427 428 429def CleanUpRemoteCvd(ssh_obj, remote_dir, raise_error): 430 """Call stop_cvd and delete the files on a remote host or a GCE instance. 431 432 Args: 433 ssh_obj: An Ssh object. 434 remote_dir: The remote base directory. 435 raise_error: Whether to raise an error if the remote instance is not 436 running. 437 438 Raises: 439 subprocess.CalledProcessError if any command fails. 440 """ 441 home = remote_path.join("$HOME", remote_dir) 442 stop_cvd_path = remote_path.join(remote_dir, "bin", "stop_cvd") 443 stop_cvd_cmd = f"'HOME={home} {stop_cvd_path}'" 444 if raise_error: 445 ssh_obj.Run(stop_cvd_cmd) 446 else: 447 try: 448 ssh_obj.Run(stop_cvd_cmd, retry=0) 449 except Exception as e: 450 logger.debug( 451 "Failed to stop_cvd (possibly no running device): %s", e) 452 453 # This command deletes all files except hidden files under HOME. 454 # It does not raise an error if no files can be deleted. 455 ssh_obj.Run(f"'rm -rf {remote_path.join(remote_dir, '*')}'") 456 457 458def GetRemoteHostBaseDir(base_instance_num): 459 """Get remote base directory by instance number. 460 461 Args: 462 base_instance_num: Integer or None, the instance number of the device. 463 464 Returns: 465 The remote base directory. 466 """ 467 return _REMOTE_HOST_BASE_DIR_FORMAT % {"num": base_instance_num or 1} 468 469 470def FormatRemoteHostInstanceName(ip_addr, base_instance_num, build_id, 471 build_target): 472 """Convert an IP address and build info to an instance name. 473 474 Args: 475 ip_addr: String, the IP address of the remote host. 476 base_instance_num: Integer or None, the instance number of the device. 477 build_id: String, the build id. 478 build_target: String, the build target, e.g., aosp_cf_x86_64_phone. 479 480 Return: 481 String, the instance name. 482 """ 483 return _REMOTE_HOST_INSTANCE_NAME_FORMAT % { 484 "ip_addr": ip_addr, 485 "num": base_instance_num or 1, 486 "build_id": build_id, 487 "build_target": build_target} 488 489 490def ParseRemoteHostAddress(instance_name): 491 """Parse IP address from a remote host instance name. 492 493 Args: 494 instance_name: String, the instance name. 495 496 Returns: 497 The IP address and the base directory as strings. 498 None if the name does not represent a remote host instance. 499 """ 500 match = _REMOTE_HOST_INSTANCE_NAME_PATTERN.fullmatch(instance_name) 501 if match: 502 return (match.group("ip_addr"), 503 GetRemoteHostBaseDir(int(match.group("num")))) 504 return None 505 506 507# pylint:disable=too-many-branches 508def GetLaunchCvdArgs(avd_spec, config=None): 509 """Get launch_cvd arguments for remote instances. 510 511 Args: 512 avd_spec: An AVDSpec instance. 513 config: A string, the name of the predefined hardware config. 514 e.g., "auto", "phone", and "tv". 515 516 Returns: 517 A list of strings, arguments of launch_cvd. 518 """ 519 launch_cvd_args = [] 520 521 blank_data_disk_size_gb = avd_spec.cfg.extra_data_disk_size_gb 522 if blank_data_disk_size_gb and blank_data_disk_size_gb > 0: 523 launch_cvd_args.append( 524 "-data_policy=" + _DATA_POLICY_CREATE_IF_MISSING) 525 launch_cvd_args.append( 526 "-blank_data_image_mb=" + str(blank_data_disk_size_gb * 1024)) 527 528 if config: 529 launch_cvd_args.append("-config=" + config) 530 if avd_spec.hw_customize or not config: 531 launch_cvd_args.append( 532 "-x_res=" + avd_spec.hw_property[constants.HW_X_RES]) 533 launch_cvd_args.append( 534 "-y_res=" + avd_spec.hw_property[constants.HW_Y_RES]) 535 launch_cvd_args.append( 536 "-dpi=" + avd_spec.hw_property[constants.HW_ALIAS_DPI]) 537 if constants.HW_ALIAS_DISK in avd_spec.hw_property: 538 launch_cvd_args.append( 539 "-data_policy=" + _DATA_POLICY_ALWAYS_CREATE) 540 launch_cvd_args.append( 541 "-blank_data_image_mb=" 542 + avd_spec.hw_property[constants.HW_ALIAS_DISK]) 543 if constants.HW_ALIAS_CPUS in avd_spec.hw_property: 544 launch_cvd_args.append( 545 "-cpus=" + str(avd_spec.hw_property[constants.HW_ALIAS_CPUS])) 546 if constants.HW_ALIAS_MEMORY in avd_spec.hw_property: 547 launch_cvd_args.append( 548 "-memory_mb=" + 549 str(avd_spec.hw_property[constants.HW_ALIAS_MEMORY])) 550 551 if avd_spec.connect_webrtc: 552 launch_cvd_args.extend(_WEBRTC_ARGS) 553 if avd_spec.webrtc_device_id: 554 launch_cvd_args.append( 555 _WEBRTC_ID % {"instance": avd_spec.webrtc_device_id}) 556 if avd_spec.connect_vnc: 557 launch_cvd_args.extend(_VNC_ARGS) 558 if avd_spec.openwrt: 559 launch_cvd_args.append(_ENABLE_CONSOLE_ARG) 560 if avd_spec.num_avds_per_instance > 1: 561 launch_cvd_args.append( 562 _NUM_AVDS_ARG % {"num_AVD": avd_spec.num_avds_per_instance}) 563 if avd_spec.base_instance_num: 564 launch_cvd_args.append( 565 "--base-instance-num=" + str(avd_spec.base_instance_num)) 566 if avd_spec.launch_args: 567 launch_cvd_args.append(avd_spec.launch_args) 568 569 launch_cvd_args.append(UNDEFOK_ARG) 570 launch_cvd_args.append(AGREEMENT_PROMPT_ARG) 571 return launch_cvd_args 572 573 574def _GetRemoteRuntimeDirs(ssh_obj, remote_dir, base_instance_num, 575 num_avds_per_instance): 576 """Get cuttlefish runtime directories on a remote host or a GCE instance. 577 578 Args: 579 ssh_obj: An Ssh object. 580 remote_dir: The remote base directory. 581 base_instance_num: An integer, the instance number of the first device. 582 num_avds_per_instance: An integer, the number of devices. 583 584 Returns: 585 A list of strings, the paths to the runtime directories. 586 """ 587 runtime_dir = remote_path.join( 588 remote_dir, _REMOTE_RUNTIME_DIR_FORMAT % {"num": base_instance_num}) 589 try: 590 ssh_obj.Run(f"test -d {runtime_dir}", retry=0) 591 return [remote_path.join(remote_dir, 592 _REMOTE_RUNTIME_DIR_FORMAT % 593 {"num": base_instance_num + num}) 594 for num in range(num_avds_per_instance)] 595 except subprocess.CalledProcessError: 596 logger.debug("%s is not the runtime directory.", runtime_dir) 597 598 legacy_runtime_dirs = [ 599 remote_path.join(remote_dir, constants.REMOTE_LOG_FOLDER)] 600 legacy_runtime_dirs.extend( 601 remote_path.join(remote_dir, 602 _REMOTE_LEGACY_RUNTIME_DIR_FORMAT % 603 {"num": base_instance_num + num}) 604 for num in range(1, num_avds_per_instance)) 605 return legacy_runtime_dirs 606 607 608def GetRemoteFetcherConfigJson(remote_dir): 609 """Get the config created by fetch_cvd on a remote host or a GCE instance. 610 611 Args: 612 remote_dir: The remote base directory. 613 614 Returns: 615 An object of report.LogFile. 616 """ 617 return report.LogFile(remote_path.join(remote_dir, "fetcher_config.json"), 618 constants.LOG_TYPE_CUTTLEFISH_LOG) 619 620 621def _GetRemoteTombstone(runtime_dir, name_suffix): 622 """Get log object for tombstones in a remote cuttlefish runtime directory. 623 624 Args: 625 runtime_dir: The path to the remote cuttlefish runtime directory. 626 name_suffix: The string appended to the log name. It is used to 627 distinguish log files found in different runtime_dirs. 628 629 Returns: 630 A report.LogFile object. 631 """ 632 return report.LogFile(remote_path.join(runtime_dir, "tombstones"), 633 constants.LOG_TYPE_DIR, 634 "tombstones-zip" + name_suffix) 635 636 637def _GetLogType(file_name): 638 """Determine log type by file name. 639 640 Args: 641 file_name: A file name. 642 643 Returns: 644 A string, one of the log types defined in constants. 645 None if the file is not a log file. 646 """ 647 if file_name == "kernel.log": 648 return constants.LOG_TYPE_KERNEL_LOG 649 if file_name == "logcat": 650 return constants.LOG_TYPE_LOGCAT 651 if file_name.endswith(".log") or file_name == "cuttlefish_config.json": 652 return constants.LOG_TYPE_CUTTLEFISH_LOG 653 return None 654 655 656def FindRemoteLogs(ssh_obj, remote_dir, base_instance_num, 657 num_avds_per_instance): 658 """Find log objects on a remote host or a GCE instance. 659 660 Args: 661 ssh_obj: An Ssh object. 662 remote_dir: The remote base directory. 663 base_instance_num: An integer or None, the instance number of the first 664 device. 665 num_avds_per_instance: An integer or None, the number of devices. 666 667 Returns: 668 A list of report.LogFile objects. 669 """ 670 runtime_dirs = _GetRemoteRuntimeDirs( 671 ssh_obj, remote_dir, 672 (base_instance_num or 1), (num_avds_per_instance or 1)) 673 logs = [] 674 for log_path in utils.FindRemoteFiles(ssh_obj, runtime_dirs): 675 file_name = remote_path.basename(log_path) 676 log_type = _GetLogType(file_name) 677 if not log_type: 678 continue 679 base, ext = remote_path.splitext(file_name) 680 # The index of the runtime_dir containing log_path. 681 index_str = "" 682 for index, runtime_dir in enumerate(runtime_dirs): 683 if log_path.startswith(runtime_dir + remote_path.sep): 684 index_str = "." + str(index) if index else "" 685 log_name = ("full_gce_logcat" + index_str if file_name == "logcat" else 686 base + index_str + ext) 687 688 logs.append(report.LogFile(log_path, log_type, log_name)) 689 690 logs.extend(_GetRemoteTombstone(runtime_dir, 691 ("." + str(index) if index else "")) 692 for index, runtime_dir in enumerate(runtime_dirs)) 693 return logs 694 695 696def FindLocalLogs(runtime_dir, instance_num): 697 """Find log objects in a local runtime directory. 698 699 Args: 700 runtime_dir: A string, the runtime directory path. 701 instance_num: An integer, the instance number. 702 703 Returns: 704 A list of report.LogFile. 705 """ 706 log_dir = _LOCAL_LOG_DIR_FORMAT % {"runtime_dir": runtime_dir, 707 "num": instance_num} 708 if not os.path.isdir(log_dir): 709 log_dir = runtime_dir 710 711 logs = [] 712 for parent_dir, _, file_names in os.walk(log_dir, followlinks=False): 713 for file_name in file_names: 714 log_path = os.path.join(parent_dir, file_name) 715 log_type = _GetLogType(file_name) 716 if os.path.islink(log_path) or not log_type: 717 continue 718 logs.append(report.LogFile(log_path, log_type)) 719 return logs 720 721 722def GetRemoteBuildInfoDict(avd_spec): 723 """Convert remote build infos to a dictionary for reporting. 724 725 Args: 726 avd_spec: An AvdSpec object containing the build infos. 727 728 Returns: 729 A dict containing the build infos. 730 """ 731 build_info_dict = { 732 key: val for key, val in avd_spec.remote_image.items() if val} 733 734 # kernel_target has a default value. If the user provides kernel_build_id 735 # or kernel_branch, then convert kernel build info. 736 if (avd_spec.kernel_build_info.get(constants.BUILD_ID) or 737 avd_spec.kernel_build_info.get(constants.BUILD_BRANCH)): 738 build_info_dict.update( 739 {"kernel_" + key: val 740 for key, val in avd_spec.kernel_build_info.items() if val} 741 ) 742 build_info_dict.update( 743 {"system_" + key: val 744 for key, val in avd_spec.system_build_info.items() if val} 745 ) 746 build_info_dict.update( 747 {"bootloader_" + key: val 748 for key, val in avd_spec.bootloader_build_info.items() if val} 749 ) 750 return build_info_dict 751 752 753def GetMixBuildTargetFilename(build_target, build_id): 754 """Get the mix build target filename. 755 756 Args: 757 build_id: String, Build id, e.g. "2263051", "P2804227" 758 build_target: String, the build target, e.g. cf_x86_phone-userdebug 759 760 Returns: 761 String, a file name, e.g. "cf_x86_phone-target_files-2263051.zip" 762 """ 763 return _DOWNLOAD_MIX_IMAGE_NAME.format( 764 build_target=build_target.split('-')[0], 765 build_id=build_id) 766 767 768def FindMiscInfo(image_dir): 769 """Find misc info in build output dir or extracted target files. 770 771 Args: 772 image_dir: The directory to search for misc info. 773 774 Returns: 775 image_dir if the directory structure looks like an output directory 776 in build environment. 777 image_dir/META if it looks like extracted target files. 778 779 Raises: 780 errors.CheckPathError if this function cannot find misc info. 781 """ 782 misc_info_path = os.path.join(image_dir, _MISC_INFO_FILE_NAME) 783 if os.path.isfile(misc_info_path): 784 return misc_info_path 785 misc_info_path = os.path.join(image_dir, _TARGET_FILES_META_DIR_NAME, 786 _MISC_INFO_FILE_NAME) 787 if os.path.isfile(misc_info_path): 788 return misc_info_path 789 raise errors.CheckPathError( 790 f"Cannot find {_MISC_INFO_FILE_NAME} in {image_dir}. The " 791 f"directory is expected to be an extracted target files zip or " 792 f"{constants.ENV_ANDROID_PRODUCT_OUT}.") 793 794 795def FindImageDir(image_dir): 796 """Find images in build output dir or extracted target files. 797 798 Args: 799 image_dir: The directory to search for images. 800 801 Returns: 802 image_dir if the directory structure looks like an output directory 803 in build environment. 804 image_dir/IMAGES if it looks like extracted target files. 805 806 Raises: 807 errors.GetLocalImageError if this function cannot find any image. 808 """ 809 if glob.glob(os.path.join(image_dir, "*.img")): 810 return image_dir 811 subdir = os.path.join(image_dir, _TARGET_FILES_IMAGES_DIR_NAME) 812 if glob.glob(os.path.join(subdir, "*.img")): 813 return subdir 814 raise errors.GetLocalImageError( 815 "Cannot find images in %s." % image_dir) 816 817 818def IsArmImage(image): 819 """Check if the image is built for ARM. 820 821 Args: 822 image: Image meta info. 823 824 Returns: 825 A boolean, whether the image is for ARM. 826 """ 827 return _ARM_TARGET_PATTERN in image.get("build_target", "") 828 829 830def FindVendorImages(image_dir): 831 """Find vendor, vendor_dlkm, odm, and odm_dlkm image in build output dir. 832 833 Args: 834 image_dir: The directory to search for images. 835 836 Returns: 837 An object of VendorImagePaths. 838 839 Raises: 840 errors.GetLocalImageError if this function cannot find images. 841 """ 842 843 image_paths = [] 844 for image_name in _VENDOR_IMAGE_NAMES: 845 image_path = os.path.join(image_dir, image_name) 846 if not os.path.isfile(image_path): 847 raise errors.GetLocalImageError( 848 f"Cannot find {image_path} in {image_dir}.") 849 image_paths.append(image_path) 850 851 return VendorImagePaths(*image_paths) 852