Merge "Migrate Test Targets to New Android Ownership Model" into main
diff --git a/create/avd_spec.py b/create/avd_spec.py
index f5da7fe..8b2f984 100644
--- a/create/avd_spec.py
+++ b/create/avd_spec.py
@@ -139,6 +139,7 @@
self._ota_build_info = {}
self._host_package_build_info = {}
self._bootloader_build_info = {}
+ self._android_efi_loader_build_info = {}
self._hw_property = None
self._hw_customize = False
self._remote_host = None
@@ -672,6 +673,9 @@
constants.BUILD_ID: args.bootloader_build_id,
constants.BUILD_BRANCH: args.bootloader_branch,
constants.BUILD_TARGET: args.bootloader_build_target}
+ self._android_efi_loader_build_info = {
+ constants.BUILD_ID: args.android_efi_loader_build_id,
+ constants.BUILD_ARTIFACT: args.android_efi_loader_artifact}
self._host_package_build_info = {
constants.BUILD_ID: args.host_package_build_id,
constants.BUILD_BRANCH: args.host_package_branch,
@@ -963,6 +967,11 @@
return self._bootloader_build_info
@property
+ def android_efi_loader_build_info(self):
+ """Return android efi loader build info."""
+ return self._android_efi_loader_build_info
+
+ @property
def flavor(self):
"""Return flavor."""
return self._flavor
diff --git a/create/create_args.py b/create/create_args.py
index cba574d..ce2e942 100644
--- a/create/create_args.py
+++ b/create/create_args.py
@@ -19,6 +19,7 @@
import argparse
import logging
import os
+import posixpath as remote_path
from acloud import errors
from acloud.create import create_common
@@ -173,6 +174,18 @@
help="'cuttlefish only' Bootloader build target.",
required=False)
parser.add_argument(
+ "--android-efi-loader-build-id",
+ type=str,
+ dest="android_efi_loader_build_id",
+ help="'cuttlefish only' Android EFI loader build id, e.g. P2804227",
+ required=False)
+ parser.add_argument(
+ "--android-efi-loader-artifact",
+ type=str,
+ dest="android_efi_loader_artifact",
+ help="'cuttlefish only' Android EFI loader artifact name, e.g. gbl_aarch64.efi",
+ required=False)
+ parser.add_argument(
"--kernel-build-id",
type=str,
dest="kernel_build_id",
@@ -882,9 +895,14 @@
raise errors.UnsupportedCreateArgs(
"--host-ssh-private-key-path is only supported for remote host.")
- if args.remote_image_dir and args.remote_host is None:
- raise errors.UnsupportedCreateArgs(
- "--remote-image-dir is only supported for remote host.")
+ if args.remote_image_dir:
+ if args.remote_host is None:
+ raise errors.UnsupportedCreateArgs(
+ "--remote-image-dir is only supported for remote host.")
+ if remote_path.basename(
+ remote_path.normpath(args.remote_image_dir)) in ("..", "."):
+ raise errors.UnsupportedCreateArgs(
+ "--remote-image-dir must not include the working directory.")
def _VerifyGoldfishArgs(args):
diff --git a/create/remote_image_local_instance.py b/create/remote_image_local_instance.py
index a49b63d..e8feaf3 100644
--- a/create/remote_image_local_instance.py
+++ b/create/remote_image_local_instance.py
@@ -122,6 +122,7 @@
avd_spec.kernel_build_info,
avd_spec.boot_build_info,
avd_spec.bootloader_build_info,
+ avd_spec.android_efi_loader_build_info,
avd_spec.ota_build_info,
avd_spec.host_package_build_info)
diff --git a/internal/lib/android_build_client.py b/internal/lib/android_build_client.py
index 05e08bc..4960bd0 100644
--- a/internal/lib/android_build_client.py
+++ b/internal/lib/android_build_client.py
@@ -66,9 +66,12 @@
LATEST = "latest"
# FETCH_CVD variables.
FETCHER_NAME = "fetch_cvd"
- FETCHER_BRANCH = "aosp-master"
- FETCHER_BUILD_TARGET = "aosp_cf_x86_64_phone-userdebug"
- FETCHER_ARM_VERSION_BUILD_TARGET = "aosp_cf_arm64_phone-userdebug"
+ FETCHER_BUILD_TARGET = "aosp_cf_x86_64_phone-trunk_staging-userdebug"
+ FETCHER_BUILD_TARGET_ARM = "aosp_cf_arm64_only_phone-trunk_staging-userdebug"
+ # TODO(b/297085994): cvd fetch is migrating from AOSP to github artifacts, so
+ # temporary returning hardcoded values instead of LKGB
+ FETCHER_BUILD_ID = 11559438
+ FETCHER_BUILD_ID_ARM = 11559085
MAX_RETRY = 3
RETRY_SLEEP_SECS = 3
@@ -128,9 +131,9 @@
is_arm_version: is ARM version fetch_cvd.
"""
if fetch_cvd_version == constants.LKGB:
- fetch_cvd_version = self.GetFetcherVersion()
+ fetch_cvd_version = self.GetFetcherVersion(is_arm_version)
fetch_cvd_build_target = (
- self.FETCHER_ARM_VERSION_BUILD_TARGET if is_arm_version
+ self.FETCHER_BUILD_TARGET_ARM if is_arm_version
else self.FETCHER_BUILD_TARGET)
try:
utils.RetryExceptionType(
@@ -162,11 +165,12 @@
os.chmod(local_dest, fetch_cvd_stat.st_mode | stat.S_IEXEC)
@staticmethod
- def ProcessBuild(build_info):
+ def ProcessBuild(build_info, ignore_artifact=False):
"""Create a Cuttlefish fetch_cvd build string.
Args:
build_info: The dictionary that contains build information.
+ ignore_artifact: Avoid adding artifact part to fetch_cvd build string
Returns:
A string, used in the fetch_cvd cmd or None if all args are None.
@@ -174,17 +178,22 @@
build_id = build_info.get(constants.BUILD_ID)
build_target = build_info.get(constants.BUILD_TARGET)
branch = build_info.get(constants.BUILD_BRANCH)
- if not build_target:
- return build_id or branch
+ artifact = build_info.get(constants.BUILD_ARTIFACT)
- if build_target and not branch:
- branch = _DEFAULT_BRANCH
- return (build_id or branch) + "/" + build_target
+ result = build_id or branch
+ if build_target is not None:
+ result = result or _DEFAULT_BRANCH
+ result += "/" + build_target
+
+ if not ignore_artifact and artifact:
+ result += "{" + artifact + "}"
+
+ return result
def GetFetchBuildArgs(self, default_build_info, system_build_info,
kernel_build_info, boot_build_info,
- bootloader_build_info, ota_build_info,
- host_package_build_info):
+ bootloader_build_info, android_efi_loader_build_info,
+ ota_build_info, host_package_build_info):
"""Get args from build information for fetch_cvd.
Each build_info is a dictionary that contains 3 items, for example,
@@ -203,6 +212,7 @@
constants.BUILD_ARTIFACT which is mapped to the
boot image name.
bootloader_build_info: The build that provides the bootloader.
+ android_efi_loader_build_info: The build that provides the Android EFI loader.
ota_build_info: The build that provides the OTA tools.
host_package_build_info: The build that provides the host package.
@@ -220,10 +230,13 @@
bootloader_build = self.ProcessBuild(bootloader_build_info)
if bootloader_build:
fetch_cvd_args.append(f"-bootloader_build={bootloader_build}")
+ android_efi_loader_build = self.ProcessBuild(android_efi_loader_build_info)
+ if android_efi_loader_build:
+ fetch_cvd_args.append(f"-android_efi_loader_build {android_efi_loader_build}")
kernel_build = self.GetKernelBuild(kernel_build_info)
if kernel_build:
fetch_cvd_args.append(f"-kernel_build={kernel_build}")
- boot_build = self.ProcessBuild(boot_build_info)
+ boot_build = self.ProcessBuild(boot_build_info, ignore_artifact=True)
if boot_build:
fetch_cvd_args.append(f"-boot_build={boot_build}")
boot_artifact = boot_build_info.get(constants.BUILD_ARTIFACT)
@@ -238,13 +251,16 @@
return fetch_cvd_args
- def GetFetcherVersion(self):
+ def GetFetcherVersion(self, is_arm_version=False):
"""Get fetch_cvd build id from LKGB.
Returns:
The build id of fetch_cvd.
"""
- return self.GetLKGB(self.FETCHER_BUILD_TARGET, self.FETCHER_BRANCH)
+ # TODO(b/297085994): currently returning hardcoded values
+ # For more information, please check the BUILD_ID constant definition
+ # comment section
+ return self.FETCHER_BUILD_ID_ARM if is_arm_version else self.FETCHER_BUILD_ID
@staticmethod
# pylint: disable=broad-except
diff --git a/internal/lib/android_build_client_test.py b/internal/lib/android_build_client_test.py
index 30e1df4..247a2f7 100644
--- a/internal/lib/android_build_client_test.py
+++ b/internal/lib/android_build_client_test.py
@@ -179,6 +179,10 @@
ota_build = {constants.BUILD_ID: "4567",
constants.BUILD_BRANCH: "ota_branch",
constants.BUILD_TARGET: "ota_target"}
+ bootloader_build = {constants.BUILD_ID: "10111213",
+ constants.BUILD_TARGET: "boot_crosvm_x86_64"}
+ android_efi_loader_build = {constants.BUILD_ID: "6789",
+ constants.BUILD_ARTIFACT: "gbl_x86_32.efi"}
boot_build = {constants.BUILD_ID: "5678",
constants.BUILD_BRANCH: "boot_branch",
constants.BUILD_TARGET: "boot_target",
@@ -192,7 +196,7 @@
self.assertEqual(
expected_args,
self.client.GetFetchBuildArgs(
- default_build, {}, {}, {}, {}, {}, {}))
+ default_build, {}, {}, {}, {}, {}, {}, {}))
# Test base image with system image.
expected_args = ["-default_build=1234/base_target",
@@ -200,7 +204,7 @@
self.assertEqual(
expected_args,
self.client.GetFetchBuildArgs(
- default_build, system_build, {}, {}, {}, {}, {}))
+ default_build, system_build, {}, {}, {}, {}, {}, {}))
# Test base image with kernel image.
expected_args = ["-default_build=1234/base_target",
@@ -208,7 +212,7 @@
self.assertEqual(
expected_args,
self.client.GetFetchBuildArgs(
- default_build, {}, kernel_build, {}, {}, {}, {}))
+ default_build, {}, kernel_build, {}, {}, {}, {}, {}))
# Test base image with boot image.
expected_args = ["-default_build=1234/base_target",
@@ -217,7 +221,23 @@
self.assertEqual(
expected_args,
self.client.GetFetchBuildArgs(
- default_build, {}, {}, boot_build, {}, {}, {}))
+ default_build, {}, {}, boot_build, {}, {}, {}, {}))
+
+ # Test base image with bootloader.
+ expected_args = ["-default_build=1234/base_target",
+ "-bootloader_build=10111213/boot_crosvm_x86_64"]
+ self.assertEqual(
+ expected_args,
+ self.client.GetFetchBuildArgs(
+ default_build, {}, {}, {}, bootloader_build, {}, {}, {}))
+
+ # Test base image with android efi.
+ expected_args = ["-default_build=1234/base_target",
+ "-android_efi_loader_build 6789{gbl_x86_32.efi}"]
+ self.assertEqual(
+ expected_args,
+ self.client.GetFetchBuildArgs(
+ default_build, {}, {}, {}, {}, android_efi_loader_build, {}, {}))
# Test base image with otatools.
expected_args = ["-default_build=1234/base_target",
@@ -225,7 +245,7 @@
self.assertEqual(
expected_args,
self.client.GetFetchBuildArgs(
- default_build, {}, {}, {}, {}, ota_build, {}))
+ default_build, {}, {}, {}, {}, {}, ota_build, {}))
# Test base image with host_package.
expected_args = ["-default_build=1234/base_target",
@@ -233,7 +253,7 @@
self.assertEqual(
expected_args,
self.client.GetFetchBuildArgs(
- default_build, {}, {}, {}, {}, {}, host_package_build))
+ default_build, {}, {}, {}, {}, {}, {}, host_package_build))
def testGetFetchCertArg(self):
"""Test GetFetchCertArg."""
diff --git a/internal/lib/cvd_compute_client_multi_stage.py b/internal/lib/cvd_compute_client_multi_stage.py
index 6e72667..c6ba1b9 100644
--- a/internal/lib/cvd_compute_client_multi_stage.py
+++ b/internal/lib/cvd_compute_client_multi_stage.py
@@ -325,7 +325,7 @@
@utils.TimeExecute(function_description="Downloading build on instance")
def FetchBuild(self, default_build_info, system_build_info,
kernel_build_info, boot_build_info, bootloader_build_info,
- ota_build_info, host_package_build_info):
+ android_efi_loader_build_info, ota_build_info, host_package_build_info):
"""Execute fetch_cvd on the remote instance to get Cuttlefish runtime files.
Args:
@@ -334,6 +334,7 @@
kernel_build_info: The build that provides the kernel.
boot_build_info: The build that provides the boot image.
bootloader_build_info: The build that provides the bootloader.
+ android_efi_loader_build_info: The build that provides the Android EFI app.
ota_build_info: The build that provides the OTA tools.
host_package_build_info: The build that provides the host package.
@@ -344,8 +345,8 @@
fetch_cvd_args = ["-credential_source=gce"]
fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
default_build_info, system_build_info, kernel_build_info,
- boot_build_info, bootloader_build_info, ota_build_info,
- host_package_build_info)
+ boot_build_info, bootloader_build_info, android_efi_loader_build_info,
+ ota_build_info, host_package_build_info)
fetch_cvd_args.extend(fetch_cvd_build_args)
self._ssh.Run("./fetch_cvd " + " ".join(fetch_cvd_args),
diff --git a/internal/lib/cvd_runtime_config_test.py b/internal/lib/cvd_runtime_config_test.py
index ae77ef0..a702ce0 100644
--- a/internal/lib/cvd_runtime_config_test.py
+++ b/internal/lib/cvd_runtime_config_test.py
@@ -162,15 +162,15 @@
def testGetIdFromInstanceDirStr(self):
"""Test GetIdFromInstanceDirStr."""
fake_instance_dir = "/path-to-instance-dir"
- self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir), None)
+ self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir, None), None)
fake_instance_dir = "/fake-path/local-instance-1/"
- self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir), "1")
+ self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir, None), "1")
fake_home_path = "/home/fake_user/"
self.Patch(os.path, 'expanduser', return_value=fake_home_path)
fake_instance_dir = "/home/fake_user/local-instance/"
- self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir), "1")
+ self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir, None), "1")
if __name__ == "__main__":
diff --git a/internal/lib/cvd_utils.py b/internal/lib/cvd_utils.py
index bb6804c..9d5a9b7 100644
--- a/internal/lib/cvd_utils.py
+++ b/internal/lib/cvd_utils.py
@@ -17,12 +17,16 @@
import collections
import fnmatch
import glob
+import json
import logging
import os
import posixpath as remote_path
+import random
import re
+import shlex
import subprocess
import tempfile
+import time
import zipfile
from acloud import errors
@@ -75,6 +79,11 @@
_REMOTE_EXTRA_IMAGE_DIR, _INITRAMFS_IMAGE_NAME)
_REMOTE_SUPER_IMAGE_PATH = remote_path.join(
_REMOTE_EXTRA_IMAGE_DIR, _SUPER_IMAGE_NAME)
+# The symbolic link to --remote-image-dir. It's in the base directory.
+_IMAGE_DIR_LINK_NAME = "image_dir_link"
+# The text file contains the number of references to --remote-image-dir.
+# Th path is --remote-image-dir + EXT.
+_REF_CNT_FILE_EXT = ".lock"
# Remote host instance name
_REMOTE_HOST_INSTANCE_NAME_FORMAT = (
@@ -314,7 +323,8 @@
search_path: A path to an image file or an image directory.
Returns:
- A list of strings, the launch_cvd arguments including the remote paths.
+ A list of string pairs. Each pair consists of a launch_cvd option and a
+ remote path.
Raises:
errors.GetLocalImageError if search_path does not contain kernel
@@ -332,22 +342,22 @@
remote_image_dir, _REMOTE_INITRAMFS_IMAGE_PATH)
ssh_obj.ScpPushFile(kernel_image_path, remote_kernel_image_path)
ssh_obj.ScpPushFile(initramfs_image_path, remote_initramfs_image_path)
- return ["-kernel_path", remote_kernel_image_path,
- "-initramfs_path", remote_initramfs_image_path]
+ return [("-kernel_path", remote_kernel_image_path),
+ ("-initramfs_path", remote_initramfs_image_path)]
boot_image_path, vendor_boot_image_path = FindBootImages(search_path)
if boot_image_path:
remote_boot_image_path = remote_path.join(
remote_image_dir, _REMOTE_BOOT_IMAGE_PATH)
ssh_obj.ScpPushFile(boot_image_path, remote_boot_image_path)
- launch_cvd_args = ["-boot_image", remote_boot_image_path]
+ launch_cvd_args = [("-boot_image", remote_boot_image_path)]
if vendor_boot_image_path:
remote_vendor_boot_image_path = remote_path.join(
remote_image_dir, _REMOTE_VENDOR_BOOT_IMAGE_PATH)
ssh_obj.ScpPushFile(vendor_boot_image_path,
remote_vendor_boot_image_path)
- launch_cvd_args.extend(["-vendor_boot_image",
- remote_vendor_boot_image_path])
+ launch_cvd_args.append(("-vendor_boot_image",
+ remote_vendor_boot_image_path))
return launch_cvd_args
raise errors.GetLocalImageError(
@@ -442,12 +452,12 @@
vbmeta_image_path: The path to the vbmeta image.
Returns:
- A list of strings, the launch_cvd arguments including the remote paths.
+ A pair of strings, the launch_cvd option and the remote path.
"""
remote_vbmeta_image_path = remote_path.join(remote_image_dir,
_REMOTE_VBMETA_IMAGE_PATH)
ssh_obj.ScpPushFile(vbmeta_image_path, remote_vbmeta_image_path)
- return ["-vbmeta_image", remote_vbmeta_image_path]
+ return "-vbmeta_image", remote_vbmeta_image_path
def AreTargetFilesRequired(avd_spec):
@@ -470,7 +480,8 @@
avd_spec requires building a super image.
Returns:
- A list of strings, the launch_cvd arguments including the remote paths.
+ A list of string pairs. Each pair consists of a launch_cvd option and a
+ remote path.
Raises:
errors.GetLocalImageError if any specified image path does not exist.
@@ -498,13 +509,13 @@
with tempfile.TemporaryDirectory() as super_image_dir:
_MixSuperImage(os.path.join(super_image_dir, _SUPER_IMAGE_NAME),
avd_spec, target_files_dir, ota)
- extra_img_args += _UploadSuperImage(ssh_obj, remote_image_dir,
- super_image_dir)
+ extra_img_args.append(_UploadSuperImage(ssh_obj, remote_image_dir,
+ super_image_dir))
vbmeta_image_path = os.path.join(super_image_dir, "vbmeta.img")
ota.MakeDisabledVbmetaImage(vbmeta_image_path)
- extra_img_args += _UploadVbmetaImage(ssh_obj, remote_image_dir,
- vbmeta_image_path)
+ extra_img_args.append(_UploadVbmetaImage(ssh_obj, remote_image_dir,
+ vbmeta_image_path))
return extra_img_args
@@ -519,7 +530,7 @@
super_image_dir: The path to the directory containing the super image.
Returns:
- A list of strings, the launch_cvd arguments including the remote paths.
+ A pair of strings, the launch_cvd option and the remote path.
"""
remote_super_image_path = remote_path.join(remote_image_dir,
_REMOTE_SUPER_IMAGE_PATH)
@@ -528,8 +539,7 @@
f"{ssh_obj.GetBaseCmd(constants.SSH_BIN)} -- "
f"tar -xf - --lzop -S -C {remote_super_image_dir}")
ssh.ShellCmdWithRetry(cmd)
- launch_cvd_args = ["-super_image", remote_super_image_path]
- return launch_cvd_args
+ return "-super_image", remote_super_image_path
def CleanUpRemoteCvd(ssh_obj, remote_dir, raise_error):
@@ -544,7 +554,9 @@
Raises:
subprocess.CalledProcessError if any command fails.
"""
- # TODO(b/293966645): Find stop_cvd in --remote-image-dir.
+ # FIXME: Use the images and launch_cvd in --remote-image-dir when
+ # cuttlefish can reliably share images.
+ _DeleteRemoteImageDirLink(ssh_obj, remote_dir)
home = remote_path.join("$HOME", remote_dir)
stop_cvd_path = remote_path.join(remote_dir, "bin", "stop_cvd")
stop_cvd_cmd = f"'HOME={home} {stop_cvd_path}'"
@@ -557,7 +569,7 @@
logger.debug(
"Failed to stop_cvd (possibly no running device): %s", e)
- # This command deletes all files except hidden files under HOME.
+ # This command deletes all files except hidden files under remote_dir.
# It does not raise an error if no files can be deleted.
ssh_obj.Run(f"'rm -rf {remote_path.join(remote_dir, '*')}'")
@@ -611,6 +623,161 @@
return None
+def PrepareRemoteImageDirLink(ssh_obj, remote_dir, remote_image_dir):
+ """Create a link to a directory containing images and tools.
+
+ Args:
+ ssh_obj: An Ssh object.
+ remote_dir: The directory in which the link is created.
+ remote_image_dir: The directory that is linked to.
+ """
+ remote_link = remote_path.join(remote_dir, _IMAGE_DIR_LINK_NAME)
+
+ # If remote_image_dir is relative to HOME, compute the relative path based
+ # on remote_dir.
+ ln_cmd = ("ln -s " +
+ ("" if remote_path.isabs(remote_image_dir) else "-r ") +
+ f"{remote_image_dir} {remote_link}")
+
+ remote_ref_cnt = remote_path.normpath(remote_image_dir) + _REF_CNT_FILE_EXT
+ ref_cnt_cmd = (f"expr $(test -s {remote_ref_cnt} && "
+ f"cat {remote_ref_cnt} || echo 0) + 1 > {remote_ref_cnt}")
+
+ # `flock` creates the file automatically.
+ # This command should create its parent directory before `flock`.
+ ssh_obj.Run(shlex.quote(
+ f"mkdir -p {remote_image_dir} && flock {remote_ref_cnt} -c " +
+ shlex.quote(
+ f"mkdir -p {remote_dir} {remote_image_dir} && "
+ f"{ln_cmd} && {ref_cnt_cmd}")))
+
+
+def _DeleteRemoteImageDirLink(ssh_obj, remote_dir):
+ """Delete the directories containing images and tools.
+
+ Args:
+ ssh_obj: An Ssh object.
+ remote_dir: The directory containing the link to the image directory.
+ """
+ remote_link = remote_path.join(remote_dir, _IMAGE_DIR_LINK_NAME)
+ # This command returns an absolute path if the link exists; otherwise
+ # an empty string. It raises an exception only if connection error.
+ remote_image_dir = ssh_obj.Run(
+ shlex.quote(f"readlink -n -e {remote_link} || true"))
+ if not remote_image_dir:
+ return
+
+ remote_ref_cnt = (remote_path.normpath(remote_image_dir) +
+ _REF_CNT_FILE_EXT)
+ # `expr` returns 1 if the result is 0.
+ ref_cnt_cmd = (f"expr $(test -s {remote_ref_cnt} && "
+ f"cat {remote_ref_cnt} || echo 1) - 1 > "
+ f"{remote_ref_cnt}")
+
+ # `flock` creates the file automatically.
+ # This command should create its parent directory before `flock`.
+ ssh_obj.Run(shlex.quote(
+ f"mkdir -p {remote_image_dir} && flock {remote_ref_cnt} -c " +
+ shlex.quote(
+ f"rm -f {remote_link} && "
+ f"{ref_cnt_cmd} || "
+ f"rm -rf {remote_image_dir} {remote_ref_cnt}")))
+
+
+def LoadRemoteImageArgs(ssh_obj, remote_timestamp_path, remote_args_path,
+ deadline):
+ """Load launch_cvd arguments from a remote path.
+
+ Acloud processes using the same --remote-image-dir synchronizes on
+ remote_timestamp_path and remote_args_path in the directory. This function
+ implements the synchronization in 3 steps:
+
+ 1. This function checks whether remote_timestamp_path is empty. If it is,
+ this acloud process becomes the uploader. This function writes the upload
+ deadline to the file and returns None. The caller should upload files to
+ the --remote-image-dir and then call SaveRemoteImageArgs. The upload
+ deadline written to the file represents when this acloud process should
+ complete uploading.
+
+ 2. If remote_timestamp_path is not empty, this function reads the upload
+ deadline from it. It then waits until remote_args_path contains the
+ arguments in a valid format, or the upload deadline passes.
+
+ 3. If this function loads arguments from remote_args_path successfully,
+ it returns the arguments. Otherwise, the uploader misses the deadline. The
+ --remote-image-dir is not usable. This function raises an error. It does
+ not attempt to reset the --remote-image-dir.
+
+ Args:
+ ssh_obj: An Ssh object.
+ remote_timestamp_path: The remote path containing the time when the
+ uploader will complete.
+ remote_args_path: The remote path where the arguments are loaded.
+ deadline: The deadline written to remote_timestamp_path if this process
+ becomes the uploader.
+
+ Returns:
+ A list of string pairs, the arguments generated by UploadExtraImages.
+ None if the directory has not been initialized.
+
+ Raises:
+ errors.CreateError if timeout.
+ """
+ timeout = int(deadline - time.time())
+ if timeout <= 0:
+ raise errors.CreateError("Timed out before loading remote image args.")
+
+ timestamp_cmd = (f"test -s {remote_timestamp_path} && "
+ f"cat {remote_timestamp_path} || "
+ f"expr $(date +%s) + {timeout} > {remote_timestamp_path}")
+ upload_deadline = ssh_obj.Run(shlex.quote(
+ f"flock {remote_timestamp_path} -c " +
+ shlex.quote(timestamp_cmd))).strip()
+ if not upload_deadline:
+ return None
+
+ # Wait until remote_args_path is not empty or upload_deadline <= now.
+ wait_cmd = (f"test -s {remote_args_path} -o "
+ f"{upload_deadline} -le $(date +%s) || echo wait...")
+ timeout = deadline - time.time()
+ utils.PollAndWait(
+ lambda : ssh_obj.Run(shlex.quote(
+ f"flock {remote_args_path} -c " + shlex.quote(wait_cmd))),
+ expected_return="",
+ timeout_exception=errors.CreateError(
+ f"{remote_args_path} is not ready within {timeout} secs"),
+ timeout_secs=timeout,
+ sleep_interval_secs=10 + random.uniform(0, 5))
+
+ args_str = ssh_obj.Run(shlex.quote(
+ f"flock {remote_args_path} -c " +
+ shlex.quote(f"cat {remote_args_path}")))
+ if not args_str:
+ raise errors.CreateError(
+ f"The uploader did not meet the deadline {upload_deadline}. "
+ f"{remote_args_path} is unusable.")
+ try:
+ return json.loads(args_str)
+ except json.JSONDecodeError as e:
+ raise errors.CreateError(f"Cannot load {remote_args_path}: {e}")
+
+
+def SaveRemoteImageArgs(ssh_obj, remote_args_path, launch_cvd_args):
+ """Save launch_cvd arguments to a remote path.
+
+ Args:
+ ssh_obj: An Ssh object.
+ remote_args_path: The remote path where the arguments are saved.
+ launch_cvd_args: A list of string pairs, the arguments generated by
+ UploadExtraImages.
+ """
+ # args_str is interpreted three times by SSH, remote shell, and flock.
+ args_str = shlex.quote(json.dumps(launch_cvd_args))
+ ssh_obj.Run(shlex.quote(
+ f"flock {remote_args_path} -c " +
+ shlex.quote(f"echo {args_str} > {remote_args_path}")))
+
+
def GetConfigFromRemoteAndroidInfo(ssh_obj, remote_image_dir):
"""Get config from android-info.txt on a remote host or a GCE instance.
@@ -712,16 +879,11 @@
Returns:
A string, the launch_cvd command.
"""
- # launch_cvd requires ANDROID_HOST_OUT to be absolute.
- cmd = ([f"{constants.ENV_ANDROID_HOST_OUT}="
- f"$(readlink -n -m {avd_spec.remote_image_dir})",
- f"{constants.ENV_ANDROID_PRODUCT_OUT}="
- f"${constants.ENV_ANDROID_HOST_OUT}"]
- if avd_spec.remote_image_dir else [])
- cmd.extend(["HOME=" + remote_path.join("$HOME", remote_dir),
- remote_path.join(avd_spec.remote_image_dir or remote_dir,
- "bin", "launch_cvd"),
- "-daemon"])
+ # FIXME: Use the images and launch_cvd in avd_spec.remote_image_dir when
+ # cuttlefish can reliably share images.
+ cmd = ["HOME=" + remote_path.join("$HOME", remote_dir),
+ remote_path.join(remote_dir, "bin", "launch_cvd"),
+ "-daemon"]
cmd.extend(extra_args)
cmd.extend(_GetLaunchCvdArgs(avd_spec, config))
return " ".join(cmd)
@@ -951,6 +1113,10 @@
{"bootloader_" + key: val
for key, val in avd_spec.bootloader_build_info.items() if val}
)
+ build_info_dict.update(
+ {"android_efi_loader_" + key: val
+ for key, val in avd_spec.android_efi_loader_build_info.items() if val}
+ )
return build_info_dict
diff --git a/internal/lib/cvd_utils_test.py b/internal/lib/cvd_utils_test.py
index 34294f8..6455a77 100644
--- a/internal/lib/cvd_utils_test.py
+++ b/internal/lib/cvd_utils_test.py
@@ -159,7 +159,7 @@
local_vendor_image=None)
args = cvd_utils.UploadExtraImages(mock_ssh, "dir", mock_avd_spec,
None)
- self.assertEqual(["-boot_image", "dir/acloud_image/boot.img"],
+ self.assertEqual([("-boot_image", "dir/acloud_image/boot.img")],
args)
mock_ssh.Run.assert_called_once_with("mkdir -p dir/acloud_image")
mock_ssh.ScpPushFile.assert_called_once_with(
@@ -170,8 +170,8 @@
args = cvd_utils.UploadExtraImages(mock_ssh, "dir", mock_avd_spec,
None)
self.assertEqual(
- ["-boot_image", "dir/acloud_image/boot.img",
- "-vendor_boot_image", "dir/acloud_image/vendor_boot.img"],
+ [("-boot_image", "dir/acloud_image/boot.img"),
+ ("-vendor_boot_image", "dir/acloud_image/vendor_boot.img")],
args)
mock_ssh.Run.assert_called_once()
self.assertEqual(2, mock_ssh.ScpPushFile.call_count)
@@ -198,8 +198,8 @@
args = cvd_utils.UploadExtraImages(mock_ssh, "dir", mock_avd_spec,
None)
self.assertEqual(
- ["-kernel_path", "dir/acloud_image/kernel",
- "-initramfs_path", "dir/acloud_image/initramfs.img"],
+ [("-kernel_path", "dir/acloud_image/kernel"),
+ ("-initramfs_path", "dir/acloud_image/initramfs.img")],
args)
mock_ssh.Run.assert_called_once()
self.assertEqual(2, mock_ssh.ScpPushFile.call_count)
@@ -232,8 +232,8 @@
target_files_dir)
self.assertEqual(
- ["-super_image", "dir/acloud_image/super.img",
- "-vbmeta_image", "dir/acloud_image/vbmeta.img"],
+ [("-super_image", "dir/acloud_image/super.img"),
+ ("-vbmeta_image", "dir/acloud_image/vbmeta.img")],
args)
mock_find_ota_tools.assert_called_once_with([])
mock_ssh.Run.assert_called_once_with("mkdir -p dir/acloud_image")
@@ -261,18 +261,36 @@
def testCleanUpRemoteCvd(self):
"""Test CleanUpRemoteCvd."""
mock_ssh = mock.Mock()
+ mock_ssh.Run.side_effect = ["", "", ""]
cvd_utils.CleanUpRemoteCvd(mock_ssh, "dir", raise_error=True)
- mock_ssh.Run.assert_any_call("'HOME=$HOME/dir dir/bin/stop_cvd'")
- mock_ssh.Run.assert_any_call("'rm -rf dir/*'")
+ mock_ssh.Run.assert_has_calls([
+ mock.call("'readlink -n -e dir/image_dir_link || true'"),
+ mock.call("'HOME=$HOME/dir dir/bin/stop_cvd'"),
+ mock.call("'rm -rf dir/*'")])
+
+ mock_ssh.reset_mock()
+ mock_ssh.Run.side_effect = ["img_dir", "", "", ""]
+ cvd_utils.CleanUpRemoteCvd(mock_ssh, "dir", raise_error=True)
+ mock_ssh.Run.assert_has_calls([
+ mock.call("'readlink -n -e dir/image_dir_link || true'"),
+ mock.call("'mkdir -p img_dir && flock img_dir.lock -c '\"'\"'"
+ "rm -f dir/image_dir_link && "
+ "expr $(test -s img_dir.lock && "
+ "cat img_dir.lock || echo 1) - 1 > img_dir.lock || "
+ "rm -rf img_dir img_dir.lock'\"'\"''"),
+ mock.call("'HOME=$HOME/dir dir/bin/stop_cvd'"),
+ mock.call("'rm -rf dir/*'")])
mock_ssh.reset_mock()
mock_ssh.Run.side_effect = [
+ "",
subprocess.CalledProcessError(cmd="should raise", returncode=1)]
with self.assertRaises(subprocess.CalledProcessError):
cvd_utils.CleanUpRemoteCvd(mock_ssh, "dir", raise_error=True)
mock_ssh.reset_mock()
mock_ssh.Run.side_effect = [
+ "",
subprocess.CalledProcessError(cmd="should ignore", returncode=1),
None]
cvd_utils.CleanUpRemoteCvd(mock_ssh, "dir", raise_error=False)
@@ -309,6 +327,158 @@
"host-goldfish-192.0.2.1-5554-123456-sdk_x86_64-sdk")
self.assertIsNone(result)
+ # pylint: disable=protected-access
+ def testRemoteImageDirLink(self):
+ """Test PrepareRemoteImageDirLink and _DeleteRemoteImageDirLink."""
+ self.assertEqual(os.path, cvd_utils.remote_path)
+ with tempfile.TemporaryDirectory(prefix="cvd_utils") as temp_dir:
+ env = os.environ.copy()
+ env["HOME"] = temp_dir
+ # Execute the commands locally.
+ mock_ssh = mock.Mock()
+ mock_ssh.Run.side_effect = lambda cmd: subprocess.check_output(
+ "sh -c " + cmd, shell=True, cwd=temp_dir, env=env
+ ).decode("utf-8")
+ # Relative paths under temp_dir.
+ base_dir_name_1 = "acloud_cf_1"
+ base_dir_name_2 = "acloud_cf_2"
+ image_dir_name = "test/img"
+ rel_ref_cnt_path = "test/img.lock"
+ # Absolute paths.
+ image_dir = os.path.join(temp_dir, image_dir_name)
+ ref_cnt_path = os.path.join(temp_dir, rel_ref_cnt_path)
+ link_path_1 = os.path.join(temp_dir, base_dir_name_1,
+ "image_dir_link")
+ link_path_2 = os.path.join(temp_dir, base_dir_name_2,
+ "image_dir_link")
+ # Delete non-existing directories.
+ cvd_utils._DeleteRemoteImageDirLink(mock_ssh, base_dir_name_1)
+ mock_ssh.Run.assert_called_with(
+ f"'readlink -n -e {base_dir_name_1}/image_dir_link || true'")
+ self.assertFalse(
+ os.path.exists(os.path.join(temp_dir, base_dir_name_1)))
+ self.assertFalse(os.path.exists(image_dir))
+ self.assertFalse(os.path.exists(ref_cnt_path))
+ # Prepare the first base dir.
+ cvd_utils.PrepareRemoteImageDirLink(mock_ssh, base_dir_name_1,
+ image_dir_name)
+ mock_ssh.Run.assert_called_with(
+ f"'mkdir -p {image_dir_name} && flock {rel_ref_cnt_path} -c "
+ f"'\"'\"'mkdir -p {base_dir_name_1} {image_dir_name} && "
+ f"ln -s -r {image_dir_name} "
+ f"{base_dir_name_1}/image_dir_link && "
+ f"expr $(test -s {rel_ref_cnt_path} && "
+ f"cat {rel_ref_cnt_path} || echo 0) + 1 > "
+ f"{rel_ref_cnt_path}'\"'\"''")
+ self.assertTrue(os.path.islink(link_path_1))
+ self.assertEqual("../test/img", os.readlink(link_path_1))
+ self.assertTrue(os.path.isfile(ref_cnt_path))
+ with open(ref_cnt_path, "r", encoding="utf-8") as ref_cnt_file:
+ self.assertEqual("1\n", ref_cnt_file.read())
+ # Prepare the second base dir.
+ cvd_utils.PrepareRemoteImageDirLink(mock_ssh, base_dir_name_2,
+ image_dir_name)
+ self.assertTrue(os.path.islink(link_path_2))
+ self.assertEqual("../test/img", os.readlink(link_path_2))
+ self.assertTrue(os.path.isfile(ref_cnt_path))
+ with open(ref_cnt_path, "r", encoding="utf-8") as ref_cnt_file:
+ self.assertEqual("2\n", ref_cnt_file.read())
+ # Delete the first base dir.
+ cvd_utils._DeleteRemoteImageDirLink(mock_ssh, base_dir_name_1)
+ self.assertFalse(os.path.lexists(link_path_1))
+ self.assertTrue(os.path.isfile(ref_cnt_path))
+ with open(ref_cnt_path, "r", encoding="utf-8") as ref_cnt_file:
+ self.assertEqual("1\n", ref_cnt_file.read())
+ # Delete the second base dir.
+ cvd_utils._DeleteRemoteImageDirLink(mock_ssh, base_dir_name_2)
+ self.assertFalse(os.path.lexists(link_path_2))
+ self.assertFalse(os.path.exists(image_dir))
+ self.assertFalse(os.path.exists(ref_cnt_path))
+
+ @mock.patch("acloud.internal.lib.cvd_utils.utils.PollAndWait")
+ @mock.patch("acloud.internal.lib.cvd_utils.utils.time.time",
+ return_value=90.0)
+ def testLoadRemoteImageArgs(self, _mock_time, mock_poll_and_wait):
+ """Test LoadRemoteImageArgs."""
+ deadline = 99.9
+ self.assertEqual(os.path, cvd_utils.remote_path)
+
+ with tempfile.TemporaryDirectory(prefix="cvd_utils") as temp_dir:
+ env = os.environ.copy()
+ env["HOME"] = temp_dir
+ # Execute the commands locally.
+ mock_ssh = mock.Mock()
+ mock_ssh.Run.side_effect = lambda cmd: subprocess.check_output(
+ "sh -c " + cmd, shell=True, cwd=temp_dir, env=env, text=True)
+ mock_poll_and_wait.side_effect = lambda func, **kwargs: func()
+
+ timestamp_path = os.path.join(temp_dir, "timestamp.txt")
+ args_path = os.path.join(temp_dir, "args.txt")
+
+ # Test with an uninitialized directory.
+ args = cvd_utils.LoadRemoteImageArgs(
+ mock_ssh, timestamp_path, args_path, deadline)
+
+ self.assertIsNone(args)
+ mock_ssh.Run.assert_called_once()
+ with open(timestamp_path, "r", encoding="utf-8") as timestamp_file:
+ timestamp = timestamp_file.read().strip()
+ self.assertRegex(timestamp, r"\d+",
+ f"Invalid timestamp: {timestamp}")
+ self.assertFalse(os.path.exists(args_path))
+
+ # Test with an initialized directory and the uploader times out.
+ mock_ssh.Run.reset_mock()
+
+ with self.assertRaises(errors.CreateError):
+ cvd_utils.LoadRemoteImageArgs(
+ mock_ssh, timestamp_path, args_path, deadline)
+
+ mock_ssh.Run.assert_has_calls([
+ mock.call(f"'flock {timestamp_path} -c '\"'\"'"
+ f"test -s {timestamp_path} && "
+ f"cat {timestamp_path} || "
+ f"expr $(date +%s) + 9 > {timestamp_path}'\"'\"''"),
+ mock.call(f"'flock {args_path} -c '\"'\"'"
+ f"test -s {args_path} -o "
+ f"{timestamp} -le $(date +%s) || "
+ "echo wait...'\"'\"''"),
+ mock.call(f"'flock {args_path} -c '\"'\"'"
+ f"cat {args_path}'\"'\"''")
+ ])
+ with open(timestamp_path, "r", encoding="utf-8") as timestamp_file:
+ self.assertEqual(timestamp_file.read().strip(), timestamp)
+ self.assertEqual(os.path.getsize(args_path), 0)
+
+ # Test with an initialized directory.
+ mock_ssh.Run.reset_mock()
+ self.CreateFile(args_path, b'[["arg", "1"]]')
+
+ args = cvd_utils.LoadRemoteImageArgs(
+ mock_ssh, timestamp_path, args_path, deadline)
+
+ self.assertEqual(args, [["arg", "1"]])
+ self.assertEqual(mock_ssh.Run.call_count, 3)
+
+ def testSaveRemoteImageArgs(self):
+ """Test SaveRemoteImageArgs."""
+ with tempfile.TemporaryDirectory(prefix="cvd_utils") as temp_dir:
+ env = os.environ.copy()
+ env["HOME"] = temp_dir
+ mock_ssh = mock.Mock()
+ mock_ssh.Run.side_effect = lambda cmd: subprocess.check_call(
+ "sh -c " + cmd, shell=True, cwd=temp_dir, env=env, text=True)
+ args_path = os.path.join(temp_dir, "args.txt")
+
+ cvd_utils.SaveRemoteImageArgs(mock_ssh, args_path, [("arg", "1")])
+
+ mock_ssh.Run.assert_called_with(
+ f"'flock {args_path} -c '\"'\"'"
+ f"""echo '"'"'"'"'"'"'"'"'[["arg", "1"]]'"'"'"'"'"'"'"'"' > """
+ f"{args_path}'\"'\"''")
+ with open(args_path, "r", encoding="utf-8") as args_file:
+ self.assertEqual(args_file.read().strip(), '[["arg", "1"]]')
+
def testGetConfigFromRemoteAndroidInfo(self):
"""Test GetConfigFromRemoteAndroidInfo."""
mock_ssh = mock.Mock()
@@ -334,7 +504,6 @@
cfg=mock_cfg,
hw_customize=False,
hw_property=hw_property,
- remote_image_dir=None,
connect_webrtc=False,
connect_vnc=False,
openwrt=False,
@@ -364,7 +533,6 @@
cfg=mock_cfg,
hw_customize=True,
hw_property=hw_property,
- remote_image_dir="img_dir",
connect_webrtc=True,
webrtc_device_id="pet-name",
connect_vnc=True,
@@ -373,10 +541,7 @@
base_instance_num=3,
launch_args="--setupwizard_mode=REQUIRED")
expected_cmd = (
- "ANDROID_HOST_OUT=$(readlink -n -m img_dir) "
- "ANDROID_PRODUCT_OUT=$ANDROID_HOST_OUT "
- "HOME=$HOME/dir "
- "img_dir/bin/launch_cvd -daemon --extra args "
+ "HOME=$HOME/dir dir/bin/launch_cvd -daemon --extra args "
"-data_policy=create_if_missing -blank_data_image_mb=20480 "
"-config=phone -x_res=1080 -y_res=1920 -dpi=240 "
"-data_policy=always_create -blank_data_image_mb=10240 "
@@ -568,7 +733,8 @@
remote_image=remote_image,
kernel_build_info={"build_target": "kernel"},
system_build_info={},
- bootloader_build_info={})
+ bootloader_build_info={},
+ android_efi_loader_build_info = {})
self.assertEqual(remote_image,
cvd_utils.GetRemoteBuildInfoDict(mock_avd_spec))
@@ -584,6 +750,10 @@
"branch": "aosp_u-boot-mainline",
"build_id": "400000",
"build_target": "u-boot_crosvm_x86_64"}
+ android_efi_loader_build_info = {
+ "build_id": "500000",
+ "artifact": "gbl_aarch64.efi"
+ }
all_build_info = {
"kernel_branch": "aosp_kernel-common-android12-5.10",
"kernel_build_id": "200000",
@@ -593,14 +763,18 @@
"system_build_target": "aosp_x86_64-userdebug",
"bootloader_branch": "aosp_u-boot-mainline",
"bootloader_build_id": "400000",
- "bootloader_build_target": "u-boot_crosvm_x86_64"}
+ "bootloader_build_target": "u-boot_crosvm_x86_64",
+ "android_efi_loader_build_id": "500000",
+ "android_efi_loader_artifact": "gbl_aarch64.efi"
+ }
all_build_info.update(remote_image)
mock_avd_spec = mock.Mock(
spec=[],
remote_image=remote_image,
kernel_build_info=kernel_build_info,
system_build_info=system_build_info,
- bootloader_build_info=bootloader_build_info)
+ bootloader_build_info=bootloader_build_info,
+ android_efi_loader_build_info=android_efi_loader_build_info)
self.assertEqual(all_build_info,
cvd_utils.GetRemoteBuildInfoDict(mock_avd_spec))
diff --git a/internal/lib/goldfish_compute_client.py b/internal/lib/goldfish_compute_client.py
index c251912..9ae7922 100644
--- a/internal/lib/goldfish_compute_client.py
+++ b/internal/lib/goldfish_compute_client.py
@@ -165,8 +165,7 @@
avd_spec=None,
extra_scopes=None,
tags=None,
- launch_args=None,
- disable_external_ip=False):
+ launch_args=None):
"""Create a goldfish instance given a stable host image and a build id.
Args:
@@ -191,8 +190,6 @@
tags: A list of tags to associate with the instance. e.g.
["http-server", "https-server"]
launch_args: String of args for launch command.
- disable_external_ip: Boolean, true if instance external ip should be
- disabled.
"""
self._VerifyZoneByQuota()
self._CheckMachineSize()
@@ -261,5 +258,4 @@
zone=self._zone,
gpu=gpu,
tags=tags,
- extra_scopes=extra_scopes,
- disable_external_ip=disable_external_ip)
+ extra_scopes=extra_scopes)
diff --git a/internal/lib/goldfish_compute_client_test.py b/internal/lib/goldfish_compute_client_test.py
index e5b3981..02f649a 100644
--- a/internal/lib/goldfish_compute_client_test.py
+++ b/internal/lib/goldfish_compute_client_test.py
@@ -199,8 +199,7 @@
zone=self.ZONE,
gpu=self.GPU,
tags=self.TAGS,
- extra_scopes=self.EXTRA_SCOPES,
- disable_external_ip=False)
+ extra_scopes=self.EXTRA_SCOPES)
@mock.patch("getpass.getuser", return_value="fake_user")
def testCreateInstanceWithAvdSpec(self, _mock_user):
@@ -271,8 +270,7 @@
zone=self.ZONE,
gpu=self.GPU,
tags=self.TAGS,
- extra_scopes=self.EXTRA_SCOPES,
- disable_external_ip=False)
+ extra_scopes=self.EXTRA_SCOPES)
if __name__ == "__main__":
diff --git a/internal/lib/ssh.py b/internal/lib/ssh.py
index 53916b8..524a297 100755
--- a/internal/lib/ssh.py
+++ b/internal/lib/ssh.py
@@ -29,7 +29,7 @@
_SSH_CMD = ("-i %(rsa_key_file)s -o LogLevel=ERROR -o ControlPath=none "
"-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no")
_SSH_IDENTITY = "-l %(login_user)s %(ip_addr)s"
-_SSH_CMD_MAX_RETRY = 5
+SSH_CMD_DEFAULT_RETRY = 5
_SSH_CMD_RETRY_SLEEP = 3
_CONNECTION_TIMEOUT = 10
_MAX_REPORTED_ERROR_LINES = 10
@@ -108,6 +108,9 @@
show_output: Boolean, True to show command output in screen.
hide_error_msg: Boolean, True to hide error message.
+ Returns:
+ A string, stdout and stderr.
+
Raises:
errors.DeviceConnectionError: Failed to connect to the GCE instance.
subprocess.CalledProcessError: The process exited with an error on the instance.
@@ -145,6 +148,7 @@
if constants.ERROR_MSG_WEBRTC_NOT_SUPPORT in stdout:
raise errors.LaunchCVDFail(constants.ERROR_MSG_WEBRTC_NOT_SUPPORT)
raise subprocess.CalledProcessError(process.returncode, cmd)
+ return stdout
def _GetErrorMessage(stdout):
@@ -185,7 +189,7 @@
def ShellCmdWithRetry(cmd, timeout=None, show_output=False,
- retry=_SSH_CMD_MAX_RETRY):
+ retry=SSH_CMD_DEFAULT_RETRY):
"""Runs a shell command on remote device.
If the network is unstable and causes SSH connect fail, it will retry. When
@@ -199,12 +203,15 @@
show_output: Boolean, True to show command output in screen.
retry: Integer, the retry times.
+ Returns:
+ A string, stdout and stderr.
+
Raises:
errors.DeviceConnectionError: For any non-zero return code of remote_cmd.
errors.LaunchCVDFail: Happened on launch_cvd with specific pattern of error message.
subprocess.CalledProcessError: The process exited with an error on the instance.
"""
- utils.RetryExceptionType(
+ return utils.RetryExceptionType(
exception_types=(errors.DeviceConnectionError,
errors.LaunchCVDFail,
subprocess.CalledProcessError),
@@ -257,7 +264,7 @@
extra_args_ssh_tunnel)
def Run(self, target_command, timeout=None, show_output=False,
- retry=_SSH_CMD_MAX_RETRY):
+ retry=SSH_CMD_DEFAULT_RETRY):
"""Run a shell command over SSH on a remote instance.
Example:
@@ -273,11 +280,15 @@
timeout: Integer, the maximum time to wait for the command to respond.
show_output: Boolean, True to show command output in screen.
retry: Integer, the retry times.
+
+ Returns:
+ A string, stdout and stderr.
"""
- ShellCmdWithRetry(self.GetBaseCmd(constants.SSH_BIN) + " " + target_command,
- timeout,
- show_output,
- retry)
+ return ShellCmdWithRetry(
+ self.GetBaseCmd(constants.SSH_BIN) + " " + target_command,
+ timeout,
+ show_output,
+ retry)
def GetBaseCmd(self, execute_bin):
"""Get a base command over SSH on a remote instance.
@@ -346,7 +357,7 @@
"Ssh isn't ready in the remote instance.") from e
@utils.TimeExecute(function_description="Waiting for SSH server")
- def WaitForSsh(self, timeout=None, max_retry=_SSH_CMD_MAX_RETRY):
+ def WaitForSsh(self, timeout=None, max_retry=SSH_CMD_DEFAULT_RETRY):
"""Wait until the remote instance is ready to accept commands over SSH.
Args:
diff --git a/internal/lib/ssh_test.py b/internal/lib/ssh_test.py
index d35a608..72ba397 100644
--- a/internal/lib/ssh_test.py
+++ b/internal/lib/ssh_test.py
@@ -93,8 +93,9 @@
def testSshRunCmd(self):
"""Test ssh run command."""
self.Patch(subprocess, "Popen", return_value=self.created_subprocess)
+ self.created_subprocess.communicate.return_value = ("stdout", "")
ssh_object = ssh.Ssh(self.FAKE_IP, self.FAKE_SSH_USER, self.FAKE_SSH_PRIVATE_KEY_PATH)
- ssh_object.Run("command")
+ self.assertEqual("stdout", ssh_object.Run("command"))
expected_cmd = (
"exec /usr/bin/ssh -i /fake/acloud_rea -o LogLevel=ERROR -o ControlPath=none "
"-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "
@@ -109,11 +110,12 @@
def testSshRunCmdwithExtraArgs(self):
"""test ssh rum command with extra command."""
self.Patch(subprocess, "Popen", return_value=self.created_subprocess)
+ self.created_subprocess.communicate.return_value = ("stdout", "")
ssh_object = ssh.Ssh(self.FAKE_IP,
self.FAKE_SSH_USER,
self.FAKE_SSH_PRIVATE_KEY_PATH,
self.FAKE_EXTRA_ARGS_SSH)
- ssh_object.Run("command")
+ self.assertEqual("stdout", ssh_object.Run("command"))
expected_cmd = (
"exec /usr/bin/ssh -i /fake/acloud_rea -o LogLevel=ERROR -o ControlPath=none "
"-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "
diff --git a/public/acloud_main.py b/public/acloud_main.py
index 308ee21..3e3ef62 100644
--- a/public/acloud_main.py
+++ b/public/acloud_main.py
@@ -435,8 +435,7 @@
autoconnect=args.autoconnect,
tags=args.tags,
report_internal_ip=args.report_internal_ip,
- boot_timeout_secs=args.boot_timeout_secs,
- disable_external_ip=args.disable_external_ip)
+ boot_timeout_secs=args.boot_timeout_secs)
elif args.which == delete_args.CMD_DELETE:
reporter = delete.Run(args)
elif args.which == list_args.CMD_LIST:
@@ -482,10 +481,11 @@
EXIT_CODE = constants.EXIT_BY_ERROR
EXCEPTION_STACKTRACE = traceback.format_exc()
EXCEPTION_LOG = str(e)
- raise
- finally:
- # Log Exit event here to calculate the consuming time.
- if LOG_METRICS:
- metrics.LogExitEvent(EXIT_CODE,
- stacktrace=EXCEPTION_STACKTRACE,
- logs=EXCEPTION_LOG)
+ sys.stderr.write("Exception: %s" % (EXCEPTION_STACKTRACE))
+
+ # Log Exit event here to calculate the consuming time.
+ if LOG_METRICS:
+ metrics.LogExitEvent(EXIT_CODE,
+ stacktrace=EXCEPTION_STACKTRACE,
+ logs=EXCEPTION_LOG)
+ sys.exit(EXIT_CODE)
diff --git a/public/actions/create_goldfish_action.py b/public/actions/create_goldfish_action.py
index aae9b8b..d461743 100644
--- a/public/actions/create_goldfish_action.py
+++ b/public/actions/create_goldfish_action.py
@@ -59,7 +59,6 @@
"/home/vsoc-01/log/adb.log",
"/var/log/daemon.log"]
- #pylint: disable=too-many-locals
def __init__(self,
cfg,
build_target,
@@ -73,8 +72,7 @@
avd_spec=None,
tags=None,
branch=None,
- emulator_branch=None,
- disable_external_ip=False):
+ emulator_branch=None):
"""Initialize.
@@ -90,8 +88,6 @@
["http-server", "https-server"]
branch: String, branch of the emulator build target.
emulator_branch: String, branch of the emulator.
- disable_external_ip: Boolean, true if instance external ip should be
- disabled.
"""
self.credentials = auth.CreateCredentials(cfg)
@@ -107,7 +103,6 @@
self._blank_data_disk_size_gb = cfg.extra_data_disk_size_gb
self._extra_scopes = cfg.extra_scopes
self._tags = tags
- self._disable_external_ip = disable_external_ip
# Configure clients
self._build_client = android_build_client.AndroidBuildClient(
@@ -173,8 +168,7 @@
avd_spec=self._avd_spec,
tags=self._tags,
extra_scopes=self._extra_scopes,
- launch_args=self._cfg.launch_args,
- disable_external_ip=self._disable_external_ip)
+ launch_args=self._cfg.launch_args)
return instance
@@ -252,8 +246,7 @@
branch=None,
tags=None,
report_internal_ip=False,
- boot_timeout_secs=None,
- disable_external_ip=False):
+ boot_timeout_secs=None):
"""Create one or multiple Goldfish devices.
Args:
@@ -281,8 +274,6 @@
external ip.
boot_timeout_secs: Integer, the maximum time in seconds used to
wait for the AVD to boot.
- disable_external_ip: Boolean, true if instance external ip should be
- disabled.
Returns:
A Report instance.
@@ -302,7 +293,6 @@
report_internal_ip = avd_spec.report_internal_ip
client_adb_port = avd_spec.client_adb_port
boot_timeout_secs = avd_spec.boot_timeout_secs
- disable_external_ip = avd_spec.disable_external_ip
if not emulator_build_target:
emulator_build_target = cfg.emulator_build_target
@@ -339,10 +329,10 @@
"Creating a goldfish device in project %s, build_target: %s, "
"build_id: %s, emulator_bid: %s, emulator_branch: %s, kernel_build_id: %s, "
"kernel_branch: %s, kernel_build_target: %s, GPU: %s, num: %s, "
- "serial_log_file: %s, autoconnect: %s, disable_external_ip: %s",
- cfg.project, build_target, build_id, emulator_build_id,
- emulator_branch, kernel_build_id, kernel_branch, kernel_build_target,
- gpu, num, serial_log_file, autoconnect, disable_external_ip)
+ "serial_log_file: %s, "
+ "autoconnect: %s", cfg.project, build_target, build_id,
+ emulator_build_id, emulator_branch, kernel_build_id, kernel_branch,
+ kernel_build_target, gpu, num, serial_log_file, autoconnect)
device_factory = GoldfishDeviceFactory(
cfg, build_target, build_id,
@@ -353,8 +343,7 @@
emulator_branch=emulator_branch,
kernel_build_id=kernel_build_id,
kernel_branch=kernel_branch,
- kernel_build_target=kernel_build_target,
- disable_external_ip=disable_external_ip)
+ kernel_build_target=kernel_build_target)
return common_operations.CreateDevices("create_gf", cfg, device_factory,
num, constants.TYPE_GF,
diff --git a/public/actions/create_goldfish_action_test.py b/public/actions/create_goldfish_action_test.py
index b5d7b51..b784be4 100644
--- a/public/actions/create_goldfish_action_test.py
+++ b/public/actions/create_goldfish_action_test.py
@@ -82,7 +82,6 @@
self.avd_spec.gpu = self.GPU
self.avd_spec.serial_log_file = None
self.avd_spec.autoconnect = False
- self.avd_spec.disable_external_ip = False
def _CreateCfg(self):
"""A helper method that creates a mock configuration object."""
@@ -154,8 +153,7 @@
avd_spec=none_avd_spec,
extra_scopes=self.EXTRA_SCOPES,
tags=None,
- launch_args=self.LAUNCH_ARGS,
- disable_external_ip=False)
+ launch_args=self.LAUNCH_ARGS)
self.assertEqual(report.data, {
"devices": [
@@ -213,8 +211,7 @@
avd_spec=self.avd_spec,
extra_scopes=self.EXTRA_SCOPES,
tags=None,
- launch_args=self.LAUNCH_ARGS,
- disable_external_ip=False)
+ launch_args=self.LAUNCH_ARGS)
def testCreateDevicesWithoutBuildId(self):
"""Test CreateDevices when emulator sysimage buildid is not provided."""
@@ -280,8 +277,7 @@
avd_spec=none_avd_spec,
extra_scopes=self.EXTRA_SCOPES,
tags=None,
- launch_args=self.LAUNCH_ARGS,
- disable_external_ip=False)
+ launch_args=self.LAUNCH_ARGS)
self.assertEqual(report.data, {
"devices": [{
@@ -337,8 +333,7 @@
avd_spec=self.avd_spec,
extra_scopes=self.EXTRA_SCOPES,
tags=None,
- launch_args=self.LAUNCH_ARGS,
- disable_external_ip=False)
+ launch_args=self.LAUNCH_ARGS)
#pylint: disable=invalid-name
def testCreateDevicesWithoutEmulatorBuildId(self):
@@ -397,8 +392,7 @@
avd_spec=none_avd_spec,
extra_scopes=self.EXTRA_SCOPES,
tags=None,
- launch_args=self.LAUNCH_ARGS,
- disable_external_ip=False)
+ launch_args=self.LAUNCH_ARGS)
self.assertEqual(report.data, {
"devices": [{
@@ -454,8 +448,7 @@
avd_spec=self.avd_spec,
extra_scopes=self.EXTRA_SCOPES,
tags=None,
- launch_args=self.LAUNCH_ARGS,
- disable_external_ip=False)
+ launch_args=self.LAUNCH_ARGS)
if __name__ == "__main__":
diff --git a/public/actions/remote_host_cf_device_factory.py b/public/actions/remote_host_cf_device_factory.py
index 403e15d..134922d 100644
--- a/public/actions/remote_host_cf_device_factory.py
+++ b/public/actions/remote_host_cf_device_factory.py
@@ -41,6 +41,8 @@
_ALL_FILES = "*"
_HOME_FOLDER = os.path.expanduser("~")
_TEMP_PREFIX = "acloud_remote_host"
+_IMAGE_TIMESTAMP_FILE_NAME = "acloud_image_timestamp.txt"
+_IMAGE_ARGS_FILE_NAME = "acloud_image_args.txt"
class RemoteHostDeviceFactory(base_device_factory.BaseDeviceFactory):
@@ -90,16 +92,29 @@
A string, representing instance name.
"""
start_time = time.time()
+ self._compute_client.SetStage(constants.STAGE_SSH_CONNECT)
instance = self._InitRemotehost()
start_time = self._compute_client.RecordTime(
constants.TIME_GCE, start_time)
- process_artifacts_timestart = start_time
- image_args = self._ProcessRemoteHostArtifacts()
- start_time = self._compute_client.RecordTime(
- constants.TIME_ARTIFACT, start_time)
+ deadline = start_time + (self._avd_spec.boot_timeout_secs or
+ constants.DEFAULT_CF_BOOT_TIMEOUT)
+ self._compute_client.SetStage(constants.STAGE_ARTIFACT)
+ try:
+ image_args = self._ProcessRemoteHostArtifacts(deadline)
+ except (errors.CreateError, errors.DriverError,
+ subprocess.CalledProcessError) as e:
+ logger.exception("Fail to prepare artifacts.")
+ self._all_failures[instance] = str(e)
+ # If an SSH error or timeout happens, report the name for the
+ # caller to clean up this instance.
+ return instance
+ finally:
+ start_time = self._compute_client.RecordTime(
+ constants.TIME_ARTIFACT, start_time)
- error_msg = self._LaunchCvd(image_args, process_artifacts_timestart)
+ self._compute_client.SetStage(constants.STAGE_BOOT_UP)
+ error_msg = self._LaunchCvd(image_args, deadline)
start_time = self._compute_client.RecordTime(
constants.TIME_LAUNCH, start_time)
@@ -147,7 +162,6 @@
Returns:
A string, representing instance name.
"""
- self._compute_client.SetStage(constants.STAGE_SSH_CONNECT)
# Get product name from the img zip file name or TARGET_PRODUCT.
image_name = os.path.basename(
self._local_image_artifact) if self._local_image_artifact else ""
@@ -174,8 +188,48 @@
raise_error=False)
return instance
- def _ProcessRemoteHostArtifacts(self):
- """Process remote host artifacts.
+ def _ProcessRemoteHostArtifacts(self, deadline):
+ """Initialize or reuse the images on the remote host.
+
+ Args:
+ deadline: The timestamp when the timeout expires.
+
+ Returns:
+ A list of strings, the launch_cvd arguments.
+ """
+ remote_image_dir = self._avd_spec.remote_image_dir
+ reuse_remote_image_dir = False
+ if remote_image_dir:
+ remote_args_path = remote_path.join(remote_image_dir,
+ _IMAGE_ARGS_FILE_NAME)
+ cvd_utils.PrepareRemoteImageDirLink(
+ self._ssh, self._GetInstancePath(), remote_image_dir)
+ launch_cvd_args = cvd_utils.LoadRemoteImageArgs(
+ self._ssh,
+ remote_path.join(remote_image_dir, _IMAGE_TIMESTAMP_FILE_NAME),
+ remote_args_path, deadline)
+ if launch_cvd_args is not None:
+ logger.info("Reuse the images in %s", remote_image_dir)
+ reuse_remote_image_dir = True
+ logger.info("Create images in %s", remote_image_dir)
+
+ if not reuse_remote_image_dir:
+ launch_cvd_args = self._InitRemoteImageDir()
+
+ if remote_image_dir:
+ if not reuse_remote_image_dir:
+ cvd_utils.SaveRemoteImageArgs(self._ssh, remote_args_path,
+ launch_cvd_args)
+ # FIXME: Use the images in remote_image_dir when cuttlefish can
+ # reliably share images.
+ launch_cvd_args = self._ReplaceRemoteImageArgs(
+ launch_cvd_args, remote_image_dir, self._GetInstancePath())
+ self._CopyRemoteImageDir(remote_image_dir, self._GetInstancePath())
+
+ return [arg for arg_pair in launch_cvd_args for arg in arg_pair]
+
+ def _InitRemoteImageDir(self):
+ """Create remote host artifacts.
- If images source is local, tool will upload images from local site to
remote host.
@@ -184,10 +238,9 @@
is no permission to fetch build rom on the remote host.
Returns:
- A list of strings, the launch_cvd arguments.
+ A list of string pairs, the launch_cvd arguments generated by
+ UploadExtraImages.
"""
- # TODO(b/293966645): Check if --remote-image-dir is initialized.
- self._compute_client.SetStage(constants.STAGE_ARTIFACT)
self._ssh.Run(f"mkdir -p {self._GetArtifactPath()}")
launch_cvd_args = []
@@ -288,6 +341,7 @@
self._avd_spec.kernel_build_info,
self._avd_spec.boot_build_info,
self._avd_spec.bootloader_build_info,
+ self._avd_spec.android_efi_loader_build_info,
self._avd_spec.ota_build_info,
self._avd_spec.host_package_build_info)
@@ -314,6 +368,7 @@
self._avd_spec.kernel_build_info,
self._avd_spec.boot_build_info,
self._avd_spec.bootloader_build_info,
+ self._avd_spec.android_efi_loader_build_info,
self._avd_spec.ota_build_info,
self._avd_spec.host_package_build_info)
@@ -373,6 +428,7 @@
self._avd_spec.kernel_build_info,
self._avd_spec.boot_build_info,
self._avd_spec.bootloader_build_info,
+ self._avd_spec.android_efi_loader_build_info,
self._avd_spec.ota_build_info,
self._avd_spec.host_package_build_info)
creds_cache_file = os.path.join(_HOME_FOLDER, cfg.creds_cache_file)
@@ -407,31 +463,61 @@
logger.debug("cmd:\n %s", cmd)
ssh.ShellCmdWithRetry(cmd)
+ @staticmethod
+ def _ReplaceRemoteImageArgs(launch_cvd_args, old_dir, new_dir):
+ """Replace the prefix of launch_cvd path arguments.
+
+ Args:
+ launch_cvd_args: A list of string pairs. Each pair consists of a
+ launch_cvd option and a remote path.
+ old_dir: The prefix of the paths to be replaced.
+ new_dir: The new prefix of the paths.
+
+ Returns:
+ A list of string pairs, the replaced arguments.
+
+ Raises:
+ errors.CreateError if any path cannot be replaced.
+ """
+ if any(remote_path.isabs(path) != remote_path.isabs(old_dir) for
+ _, path in launch_cvd_args):
+ raise errors.CreateError(f"Cannot convert {launch_cvd_args} to "
+ f"relative paths under {old_dir}")
+ return [(option,
+ remote_path.join(new_dir, remote_path.relpath(path, old_dir)))
+ for option, path in launch_cvd_args]
+
+ @utils.TimeExecute(function_description="Copying images")
+ def _CopyRemoteImageDir(self, remote_src_dir, remote_dst_dir):
+ """Copy a remote directory recursively.
+
+ Args:
+ remote_src_dir: The source directory.
+ remote_dst_dir: The destination directory.
+ """
+ self._ssh.Run(f"cp -frT {remote_src_dir} {remote_dst_dir}")
+
@utils.TimeExecute(
function_description="Launching AVD(s) and waiting for boot up",
result_evaluator=utils.BootEvaluator)
- def _LaunchCvd(self, image_args, start_time):
+ def _LaunchCvd(self, image_args, deadline):
"""Execute launch_cvd.
Args:
image_args: A list of strings, the extra arguments generated by
acloud for remote image paths.
- start_time: The timestamp when the remote host is initialized.
+ deadline: The timestamp when the timeout expires.
Returns:
The error message as a string. An empty string represents success.
"""
- self._compute_client.SetStage(constants.STAGE_BOOT_UP)
config = cvd_utils.GetConfigFromRemoteAndroidInfo(
self._ssh, self._GetArtifactPath())
cmd = cvd_utils.GetRemoteLaunchCvdCmd(
self._GetInstancePath(), self._avd_spec, config, image_args)
- boot_timeout_secs = (self._avd_spec.boot_timeout_secs or
- constants.DEFAULT_CF_BOOT_TIMEOUT)
- boot_timeout_secs -= time.time() - start_time
+ boot_timeout_secs = deadline - time.time()
if boot_timeout_secs <= 0:
- return ("Timed out before launch_cvd. "
- f"Remaining time: {boot_timeout_secs} secs.")
+ return "Timed out before launch_cvd."
self._compute_client.ExtendReportData(
constants.LAUNCH_CVD_COMMAND, cmd)
diff --git a/public/actions/remote_host_cf_device_factory_test.py b/public/actions/remote_host_cf_device_factory_test.py
index 057e0bc..89c4b05 100644
--- a/public/actions/remote_host_cf_device_factory_test.py
+++ b/public/actions/remote_host_cf_device_factory_test.py
@@ -18,10 +18,12 @@
import unittest
from unittest import mock
+from acloud import errors
from acloud.internal import constants
from acloud.internal.lib import driver_test_lib
from acloud.public.actions import remote_host_cf_device_factory
+
class RemoteHostDeviceFactoryTest(driver_test_lib.BaseDriverTest):
"""Test RemoteHostDeviceFactory."""
@@ -56,6 +58,7 @@
kernel_build_info={},
boot_build_info={},
bootloader_build_info={},
+ android_efi_loader_build_info={},
ota_build_info={},
host_package_build_info={},
remote_host="192.0.2.100",
@@ -98,7 +101,7 @@
mock_cvd_utils.GetRemoteHostBaseDir.return_value = "acloud_cf_2"
mock_cvd_utils.FormatRemoteHostInstanceName.return_value = "inst"
mock_cvd_utils.AreTargetFilesRequired.return_value = True
- mock_cvd_utils.UploadExtraImages.return_value = ["extra"]
+ mock_cvd_utils.UploadExtraImages.return_value = [("-extra", "image")]
mock_cvd_utils.ExecuteRemoteLaunchCvd.return_value = "failure"
mock_cvd_utils.FindRemoteLogs.return_value = [log]
@@ -119,7 +122,7 @@
mock_ssh_obj, "acloud_cf_2")
# LaunchCvd
mock_cvd_utils.GetRemoteLaunchCvdCmd.assert_called_with(
- "acloud_cf_2", mock_avd_spec, mock.ANY, ["extra"])
+ "acloud_cf_2", mock_avd_spec, mock.ANY, ["-extra", "image"])
mock_cvd_utils.ExecuteRemoteLaunchCvd.assert_called()
# FindLogFiles
mock_cvd_utils.FindRemoteLogs.assert_called_with(
@@ -385,6 +388,91 @@
self.assertFalse(factory.GetFailures())
self.assertDictEqual({"inst": [log]}, factory.GetLogs())
+ @mock.patch("acloud.public.actions.remote_host_cf_device_factory.ssh")
+ @mock.patch("acloud.public.actions.remote_host_cf_device_factory."
+ "cvd_utils")
+ @mock.patch("acloud.public.actions.remote_host_cf_device_factory."
+ "subprocess.check_call")
+ @mock.patch("acloud.public.actions.remote_host_cf_device_factory.glob")
+ @mock.patch("acloud.public.actions.remote_host_cf_device_factory.pull")
+ def testCreateInstanceWithRemoteImageDir(self, _mock_pull, mock_glob,
+ _mock_check_call, mock_cvd_utils,
+ mock_ssh):
+ """Test CreateInstance with AvdSpec.remote_image_dir."""
+ mock_avd_spec = self._CreateMockAvdSpec()
+ mock_avd_spec.remote_image_dir = "mock_img_dir"
+
+ mock_ssh_obj = mock.Mock()
+ mock_ssh.Ssh.return_value = mock_ssh_obj
+ # Test initializing the remote image dir.
+ mock_glob.glob.return_value = ["/mock/super.img"]
+ factory = remote_host_cf_device_factory.RemoteHostDeviceFactory(
+ mock_avd_spec)
+
+ mock_cvd_utils.GetRemoteHostBaseDir.return_value = "acloud_cf_1"
+ mock_cvd_utils.FormatRemoteHostInstanceName.return_value = "inst"
+ mock_cvd_utils.LoadRemoteImageArgs.return_value = None
+ mock_cvd_utils.AreTargetFilesRequired.return_value = False
+ mock_cvd_utils.UploadExtraImages.return_value = [
+ ("arg", "mock_img_dir/1")]
+ mock_cvd_utils.ExecuteRemoteLaunchCvd.return_value = ""
+ mock_cvd_utils.FindRemoteLogs.return_value = []
+
+ self._mock_build_api.GetFetchBuildArgs.return_value = ["-test"]
+
+ self.assertEqual("inst", factory.CreateInstance())
+ mock_cvd_utils.PrepareRemoteImageDirLink.assert_called_once_with(
+ mock_ssh_obj, "acloud_cf_1", "mock_img_dir")
+ mock_cvd_utils.LoadRemoteImageArgs.assert_called_once_with(
+ mock_ssh_obj, "mock_img_dir/acloud_image_timestamp.txt",
+ "mock_img_dir/acloud_image_args.txt", mock.ANY)
+ mock_cvd_utils.SaveRemoteImageArgs.assert_called_once_with(
+ mock_ssh_obj, "mock_img_dir/acloud_image_args.txt",
+ [("arg", "mock_img_dir/1")])
+ mock_ssh_obj.Run.assert_called_with("cp -frT mock_img_dir acloud_cf_1")
+ self._mock_build_api.DownloadFetchcvd.assert_called_once()
+ self.assertEqual(["arg", "acloud_cf_1/1"],
+ mock_cvd_utils.GetRemoteLaunchCvdCmd.call_args[0][3])
+
+ # Test reusing the remote image dir.
+ mock_cvd_utils.LoadRemoteImageArgs.return_value = [
+ ["arg", "mock_img_dir/2"]]
+ mock_cvd_utils.SaveRemoteImageArgs.reset_mock()
+ mock_ssh_obj.reset_mock()
+ self._mock_build_api.DownloadFetchcvd.reset_mock()
+
+ self.assertEqual("inst", factory.CreateInstance())
+ mock_cvd_utils.SaveRemoteImageArgs.assert_not_called()
+ mock_ssh_obj.Run.assert_called_with("cp -frT mock_img_dir acloud_cf_1")
+ self._mock_build_api.DownloadFetchcvd.assert_not_called()
+ self.assertEqual(["arg", "acloud_cf_1/2"],
+ mock_cvd_utils.GetRemoteLaunchCvdCmd.call_args[0][3])
+
+ @mock.patch("acloud.public.actions.remote_host_cf_device_factory.ssh")
+ @mock.patch("acloud.public.actions.remote_host_cf_device_factory."
+ "cvd_utils")
+ @mock.patch("acloud.public.actions.remote_host_cf_device_factory."
+ "subprocess.check_call")
+ def testCreateInstanceWithCreateError(self, _mock_check_call,
+ mock_cvd_utils, mock_ssh):
+ """Test CreateInstance with CreateError."""
+ mock_avd_spec = self._CreateMockAvdSpec()
+ mock_avd_spec.remote_image_dir = "mock_img_dir"
+
+ mock_ssh_obj = mock.Mock()
+ mock_ssh.Ssh.return_value = mock_ssh_obj
+
+ mock_cvd_utils.GetRemoteHostBaseDir.return_value = "acloud_cf_1"
+ mock_cvd_utils.FormatRemoteHostInstanceName.return_value = "inst"
+ mock_cvd_utils.LoadRemoteImageArgs.side_effect = errors.CreateError(
+ "failure")
+ factory = remote_host_cf_device_factory.RemoteHostDeviceFactory(
+ mock_avd_spec)
+
+ self.assertEqual("inst", factory.CreateInstance())
+ self.assertEqual({"inst": "failure"}, factory.GetFailures())
+ mock_cvd_utils.ExecuteRemoteLaunchCvd.assert_not_called()
+
if __name__ == "__main__":
unittest.main()
diff --git a/public/actions/remote_instance_cf_device_factory.py b/public/actions/remote_instance_cf_device_factory.py
index b625999..ba618c1 100644
--- a/public/actions/remote_instance_cf_device_factory.py
+++ b/public/actions/remote_instance_cf_device_factory.py
@@ -111,6 +111,7 @@
avd_spec.kernel_build_info,
avd_spec.boot_build_info,
avd_spec.bootloader_build_info,
+ avd_spec.android_efi_loader_build_info,
avd_spec.ota_build_info,
avd_spec.host_package_build_info)
@@ -126,7 +127,7 @@
if avd_spec.extra_files:
self._compute_client.UploadExtraFiles(avd_spec.extra_files)
- return launch_cvd_args
+ return [arg for arg_pair in launch_cvd_args for arg in arg_pair]
@utils.TimeExecute(function_description="Downloading target_files archive")
def _DownloadTargetFiles(self, download_dir):
diff --git a/public/actions/remote_instance_cf_device_factory_test.py b/public/actions/remote_instance_cf_device_factory_test.py
index ecd2485..9d736a2 100644
--- a/public/actions/remote_instance_cf_device_factory_test.py
+++ b/public/actions/remote_instance_cf_device_factory_test.py
@@ -243,7 +243,7 @@
mock_cvd_utils.AreTargetFilesRequired.return_value = False
mock_cvd_utils.FindRemoteLogs.return_value = [{"path": "/logcat"}]
mock_cvd_utils.UploadExtraImages.return_value = [
- "-boot_image", "/boot/img"]
+ ("-boot_image", "/boot/img")]
fake_host_package_name = "/fake/host_package.tar.gz"
fake_image_name = ""