1#!/usr/bin/env python 2# 3# Copyright 2018 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16r"""RemoteImageLocalInstance class. 17 18Create class that is responsible for creating a local instance AVD with a 19remote image. 20""" 21import logging 22import os 23import shutil 24import subprocess 25import sys 26 27from acloud import errors 28from acloud.create import create_common 29from acloud.create import local_image_local_instance 30from acloud.internal import constants 31from acloud.internal.lib import android_build_client 32from acloud.internal.lib import auth 33from acloud.internal.lib import cvd_utils 34from acloud.internal.lib import ota_tools 35from acloud.internal.lib import utils 36from acloud.setup import setup_common 37 38 39logger = logging.getLogger(__name__) 40 41# Download remote image variables. 42_CUTTLEFISH_COMMON_BIN_PATH = "/usr/lib/cuttlefish-common/bin/" 43_CONFIRM_DOWNLOAD_DIR = ("Download dir %(download_dir)s does not have enough " 44 "space (available space %(available_space)sGB, " 45 "require %(required_space)sGB).\nPlease enter " 46 "alternate path or 'q' to exit: ") 47_HOME_FOLDER = os.path.expanduser("~") 48# The downloaded image artifacts will take up ~8G: 49# $du -lh --time $ANDROID_PRODUCT_OUT/aosp_cf_x86_phone-img-eng.XXX.zip 50# 422M 51# And decompressed becomes 7.2G (as of 11/2018). 52# Let's add an extra buffer (~2G) to make sure user has enough disk space 53# for the downloaded image artifacts. 54_REQUIRED_SPACE = 10 55 56_SYSTEM_MIX_IMAGE_DIR = "mix_image_{build_id}" 57 58 59def _ShouldClearFetchDir(fetch_dir, avd_spec, fetch_cvd_args_str, 60 fetch_cvd_args_file): 61 """Check if the previous fetch directory should be removed. 62 63 The fetch directory would be removed when the user explicitly sets the 64 --force-sync flag, or when the fetch_cvd_args_str changed. 65 66 Args: 67 fetch_dir: String, path to the fetch directory. 68 avd_spec: AVDSpec object that tells us what we're going to create. 69 fetch_cvd_args_str: String, args for current fetch_cvd command. 70 fetch_cvd_args_file: String, path to file of previous fetch_cvd args. 71 72 Returns: 73 Boolean. True if the fetch directory should be removed. 74 """ 75 if not os.path.exists(fetch_dir): 76 return False 77 if avd_spec.force_sync: 78 return True 79 80 if not os.path.exists(fetch_cvd_args_file): 81 return True 82 with open(fetch_cvd_args_file, "r") as f: 83 return fetch_cvd_args_str != f.read() 84 85 86@utils.TimeExecute(function_description="Downloading Android Build image") 87def DownloadAndProcessImageFiles(avd_spec): 88 """Download the CF image artifacts and process them. 89 90 To download rom images, Acloud would download the tool fetch_cvd that can 91 help process mixed build images, and store the fetch_cvd_build_args in 92 FETCH_CVD_ARGS_FILE when the fetch command executes successfully. Next 93 time when this function is called with the same image_download_dir, the 94 FETCH_CVD_ARGS_FILE would be used to check whether this image_download_dir 95 can be reused or not. 96 97 Args: 98 avd_spec: AVDSpec object that tells us what we're going to create. 99 100 Returns: 101 extract_path: String, path to image folder. 102 103 Raises: 104 errors.GetRemoteImageError: Fails to download rom images. 105 """ 106 cfg = avd_spec.cfg 107 build_id = avd_spec.remote_image[constants.BUILD_ID] 108 build_target = avd_spec.remote_image[constants.BUILD_TARGET] 109 110 extract_path = os.path.join( 111 avd_spec.image_download_dir, 112 constants.TEMP_ARTIFACTS_FOLDER, 113 build_id + build_target) 114 115 logger.debug("Extract path: %s", extract_path) 116 117 build_api = ( 118 android_build_client.AndroidBuildClient(auth.CreateCredentials(cfg))) 119 fetch_cvd_build_args = build_api.GetFetchBuildArgs( 120 avd_spec.remote_image, 121 avd_spec.system_build_info, 122 avd_spec.kernel_build_info, 123 avd_spec.boot_build_info, 124 avd_spec.bootloader_build_info, 125 avd_spec.android_efi_loader_build_info, 126 avd_spec.ota_build_info, 127 avd_spec.host_package_build_info) 128 129 fetch_cvd_args_str = " ".join(fetch_cvd_build_args) 130 fetch_cvd_args_file = os.path.join(extract_path, 131 constants.FETCH_CVD_ARGS_FILE) 132 if _ShouldClearFetchDir(extract_path, avd_spec, fetch_cvd_args_str, 133 fetch_cvd_args_file): 134 shutil.rmtree(extract_path) 135 136 if not os.path.exists(extract_path): 137 os.makedirs(extract_path) 138 139 # Download rom images via cvd fetch 140 fetch_cvd_args = list(constants.CMD_CVD_FETCH) 141 creds_cache_file = os.path.join(_HOME_FOLDER, cfg.creds_cache_file) 142 fetch_cvd_cert_arg = build_api.GetFetchCertArg(creds_cache_file) 143 fetch_cvd_args.extend([f"-target_directory={extract_path}", 144 fetch_cvd_cert_arg]) 145 # Android boolean parsing does not recognize capitalized True/False as valid 146 lowercase_enable_value = str(avd_spec.enable_fetch_local_caching).lower() 147 fetch_cvd_args.extend([f"-enable_caching={lowercase_enable_value}"]) 148 fetch_cvd_args.extend(fetch_cvd_build_args) 149 logger.debug("Download images command: %s", fetch_cvd_args) 150 if not setup_common.PackageInstalled(constants.CUTTLEFISH_COMMOM_PKG): 151 raise errors.NoCuttlefishCommonInstalled( 152 "cuttlefish-common package is required to run cvd fetch") 153 try: 154 subprocess.check_call(fetch_cvd_args) 155 except subprocess.CalledProcessError as e: 156 raise errors.GetRemoteImageError(f"Fails to download images: {e}") 157 158 # Save the fetch cvd build args when the fetch command succeeds 159 with open(fetch_cvd_args_file, "w") as output: 160 output.write(fetch_cvd_args_str) 161 162 return extract_path 163 164 165def ConfirmDownloadRemoteImageDir(download_dir): 166 """Confirm download remote image directory. 167 168 If available space of download_dir is less than _REQUIRED_SPACE, ask 169 the user to choose a different download dir or to exit out since acloud will 170 fail to download the artifacts due to insufficient disk space. 171 172 Args: 173 download_dir: String, a directory for download and decompress. 174 175 Returns: 176 String, Specific download directory when user confirm to change. 177 """ 178 while True: 179 download_dir = os.path.expanduser(download_dir) 180 if not os.path.exists(download_dir): 181 answer = utils.InteractWithQuestion( 182 "No such directory %s.\nEnter 'y' to create it, enter " 183 "anything else to exit out[y/N]: " % download_dir) 184 if answer.lower() == "y": 185 os.makedirs(download_dir) 186 else: 187 sys.exit(constants.EXIT_BY_USER) 188 189 stat = os.statvfs(download_dir) 190 available_space = stat.f_bavail*stat.f_bsize/(1024)**3 191 if available_space < _REQUIRED_SPACE: 192 download_dir = utils.InteractWithQuestion( 193 _CONFIRM_DOWNLOAD_DIR % {"download_dir":download_dir, 194 "available_space":available_space, 195 "required_space":_REQUIRED_SPACE}) 196 if download_dir.lower() == "q": 197 sys.exit(constants.EXIT_BY_USER) 198 else: 199 return download_dir 200 201 202class RemoteImageLocalInstance(local_image_local_instance.LocalImageLocalInstance): 203 """Create class for a remote image local instance AVD. 204 205 RemoteImageLocalInstance just defines logic in downloading the remote image 206 artifacts and leverages the existing logic to launch a local instance in 207 LocalImageLocalInstance. 208 """ 209 210 # pylint: disable=too-many-locals 211 def GetImageArtifactsPath(self, avd_spec): 212 """Download the image artifacts and return the paths to them. 213 214 Args: 215 avd_spec: AVDSpec object that tells us what we're going to create. 216 217 Raises: 218 errors.NoCuttlefishCommonInstalled: cuttlefish-common doesn't install. 219 220 Returns: 221 local_image_local_instance.ArtifactPaths object. 222 """ 223 if not setup_common.PackageInstalled("cuttlefish-common"): 224 raise errors.NoCuttlefishCommonInstalled( 225 "Package [cuttlefish-common] is not installed!\n" 226 "Please run 'acloud setup --host' to install.") 227 228 avd_spec.image_download_dir = ConfirmDownloadRemoteImageDir( 229 avd_spec.image_download_dir) 230 231 image_dir = DownloadAndProcessImageFiles(avd_spec) 232 launch_cvd_path = os.path.join(image_dir, "bin", 233 constants.CMD_LAUNCH_CVD) 234 if not os.path.exists(launch_cvd_path): 235 raise errors.GetCvdLocalHostPackageError( 236 "No launch_cvd found. Please check downloaded artifacts dir: %s" 237 % image_dir) 238 239 mix_image_dir = None 240 misc_info_path = None 241 ota_tools_dir = None 242 system_image_path = None 243 system_ext_image_path = None 244 product_image_path = None 245 boot_image_path = None 246 vendor_boot_image_path = None 247 kernel_image_path = None 248 initramfs_image_path = None 249 vendor_image_path = None 250 vendor_dlkm_image_path = None 251 odm_image_path = None 252 odm_dlkm_image_path = None 253 host_bins_path = image_dir 254 if avd_spec.local_system_image or avd_spec.local_vendor_image: 255 build_id = avd_spec.remote_image[constants.BUILD_ID] 256 build_target = avd_spec.remote_image[constants.BUILD_TARGET] 257 mix_image_dir = os.path.join( 258 image_dir, _SYSTEM_MIX_IMAGE_DIR.format(build_id=build_id)) 259 if not os.path.exists(mix_image_dir): 260 os.makedirs(mix_image_dir) 261 create_common.DownloadRemoteArtifact( 262 avd_spec.cfg, build_target, build_id, 263 cvd_utils.GetMixBuildTargetFilename( 264 build_target, build_id), 265 mix_image_dir, decompress=True) 266 misc_info_path = cvd_utils.FindMiscInfo(mix_image_dir) 267 mix_image_dir = cvd_utils.FindImageDir(mix_image_dir) 268 tool_dirs = (avd_spec.local_tool_dirs + 269 create_common.GetNonEmptyEnvVars( 270 constants.ENV_ANDROID_SOONG_HOST_OUT, 271 constants.ENV_ANDROID_HOST_OUT)) 272 ota_tools_dir = os.path.abspath( 273 ota_tools.FindOtaToolsDir(tool_dirs)) 274 275 # When using local vendor image, use cvd in local-tool if it 276 # exists. Fall back to downloaded tools in case it's missing 277 278 if avd_spec.local_vendor_image and avd_spec.local_tool_dirs: 279 try: 280 host_bins_path = self._FindCvdHostBinaries(tool_dirs) 281 except errors.GetCvdLocalHostPackageError: 282 logger.debug("fall back to downloaded cvd host binaries") 283 284 if avd_spec.local_system_image: 285 ( 286 system_image_path, 287 system_ext_image_path, 288 product_image_path, 289 ) = create_common.FindSystemImages(avd_spec.local_system_image) 290 291 if avd_spec.local_kernel_image: 292 ( 293 boot_image_path, 294 vendor_boot_image_path, 295 kernel_image_path, 296 initramfs_image_path, 297 ) = self.FindBootOrKernelImages( 298 os.path.abspath(avd_spec.local_kernel_image)) 299 300 if avd_spec.local_vendor_boot_image: 301 vendor_boot_image_path = create_common.FindVendorBootImage( 302 avd_spec.local_vendor_boot_image) 303 304 if avd_spec.local_vendor_image: 305 vendor_image_paths = cvd_utils.FindVendorImages( 306 avd_spec.local_vendor_image) 307 vendor_image_path = vendor_image_paths.vendor 308 vendor_dlkm_image_path = vendor_image_paths.vendor_dlkm 309 odm_image_path = vendor_image_paths.odm 310 odm_dlkm_image_path = vendor_image_paths.odm_dlkm 311 312 return local_image_local_instance.ArtifactPaths( 313 image_dir=mix_image_dir or image_dir, 314 host_bins=host_bins_path, 315 host_artifacts=image_dir, 316 misc_info=misc_info_path, 317 ota_tools_dir=ota_tools_dir, 318 system_image=system_image_path, 319 system_ext_image=system_ext_image_path, 320 product_image=product_image_path, 321 vendor_image=vendor_image_path, 322 vendor_dlkm_image=vendor_dlkm_image_path, 323 odm_image=odm_image_path, 324 odm_dlkm_image=odm_dlkm_image_path, 325 boot_image=boot_image_path, 326 vendor_boot_image=vendor_boot_image_path, 327 kernel_image=kernel_image_path, 328 initramfs_image=initramfs_image_path) 329