• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 - The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""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