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"-directory={extract_path}", 144 fetch_cvd_cert_arg]) 145 fetch_cvd_args.extend(fetch_cvd_build_args) 146 logger.debug("Download images command: %s", fetch_cvd_args) 147 if not setup_common.PackageInstalled(constants.CUTTLEFISH_COMMOM_PKG): 148 raise errors.NoCuttlefishCommonInstalled( 149 "cuttlefish-common package is required to run cvd fetch") 150 try: 151 subprocess.check_call(fetch_cvd_args) 152 except subprocess.CalledProcessError as e: 153 raise errors.GetRemoteImageError(f"Fails to download images: {e}") 154 155 # Save the fetch cvd build args when the fetch command succeeds 156 with open(fetch_cvd_args_file, "w") as output: 157 output.write(fetch_cvd_args_str) 158 159 return extract_path 160 161 162def ConfirmDownloadRemoteImageDir(download_dir): 163 """Confirm download remote image directory. 164 165 If available space of download_dir is less than _REQUIRED_SPACE, ask 166 the user to choose a different download dir or to exit out since acloud will 167 fail to download the artifacts due to insufficient disk space. 168 169 Args: 170 download_dir: String, a directory for download and decompress. 171 172 Returns: 173 String, Specific download directory when user confirm to change. 174 """ 175 while True: 176 download_dir = os.path.expanduser(download_dir) 177 if not os.path.exists(download_dir): 178 answer = utils.InteractWithQuestion( 179 "No such directory %s.\nEnter 'y' to create it, enter " 180 "anything else to exit out[y/N]: " % download_dir) 181 if answer.lower() == "y": 182 os.makedirs(download_dir) 183 else: 184 sys.exit(constants.EXIT_BY_USER) 185 186 stat = os.statvfs(download_dir) 187 available_space = stat.f_bavail*stat.f_bsize/(1024)**3 188 if available_space < _REQUIRED_SPACE: 189 download_dir = utils.InteractWithQuestion( 190 _CONFIRM_DOWNLOAD_DIR % {"download_dir":download_dir, 191 "available_space":available_space, 192 "required_space":_REQUIRED_SPACE}) 193 if download_dir.lower() == "q": 194 sys.exit(constants.EXIT_BY_USER) 195 else: 196 return download_dir 197 198 199class RemoteImageLocalInstance(local_image_local_instance.LocalImageLocalInstance): 200 """Create class for a remote image local instance AVD. 201 202 RemoteImageLocalInstance just defines logic in downloading the remote image 203 artifacts and leverages the existing logic to launch a local instance in 204 LocalImageLocalInstance. 205 """ 206 207 # pylint: disable=too-many-locals 208 def GetImageArtifactsPath(self, avd_spec): 209 """Download the image artifacts and return the paths to them. 210 211 Args: 212 avd_spec: AVDSpec object that tells us what we're going to create. 213 214 Raises: 215 errors.NoCuttlefishCommonInstalled: cuttlefish-common doesn't install. 216 217 Returns: 218 local_image_local_instance.ArtifactPaths object. 219 """ 220 if not setup_common.PackageInstalled("cuttlefish-common"): 221 raise errors.NoCuttlefishCommonInstalled( 222 "Package [cuttlefish-common] is not installed!\n" 223 "Please run 'acloud setup --host' to install.") 224 225 avd_spec.image_download_dir = ConfirmDownloadRemoteImageDir( 226 avd_spec.image_download_dir) 227 228 image_dir = DownloadAndProcessImageFiles(avd_spec) 229 launch_cvd_path = os.path.join(image_dir, "bin", 230 constants.CMD_LAUNCH_CVD) 231 if not os.path.exists(launch_cvd_path): 232 raise errors.GetCvdLocalHostPackageError( 233 "No launch_cvd found. Please check downloaded artifacts dir: %s" 234 % image_dir) 235 236 mix_image_dir = None 237 misc_info_path = None 238 ota_tools_dir = None 239 system_image_path = None 240 system_ext_image_path = None 241 product_image_path = None 242 boot_image_path = None 243 vendor_boot_image_path = None 244 kernel_image_path = None 245 initramfs_image_path = None 246 vendor_image_path = None 247 vendor_dlkm_image_path = None 248 odm_image_path = None 249 odm_dlkm_image_path = None 250 host_bins_path = image_dir 251 if avd_spec.local_system_image or avd_spec.local_vendor_image: 252 build_id = avd_spec.remote_image[constants.BUILD_ID] 253 build_target = avd_spec.remote_image[constants.BUILD_TARGET] 254 mix_image_dir = os.path.join( 255 image_dir, _SYSTEM_MIX_IMAGE_DIR.format(build_id=build_id)) 256 if not os.path.exists(mix_image_dir): 257 os.makedirs(mix_image_dir) 258 create_common.DownloadRemoteArtifact( 259 avd_spec.cfg, build_target, build_id, 260 cvd_utils.GetMixBuildTargetFilename( 261 build_target, build_id), 262 mix_image_dir, decompress=True) 263 misc_info_path = cvd_utils.FindMiscInfo(mix_image_dir) 264 mix_image_dir = cvd_utils.FindImageDir(mix_image_dir) 265 tool_dirs = (avd_spec.local_tool_dirs + 266 create_common.GetNonEmptyEnvVars( 267 constants.ENV_ANDROID_SOONG_HOST_OUT, 268 constants.ENV_ANDROID_HOST_OUT)) 269 ota_tools_dir = os.path.abspath( 270 ota_tools.FindOtaToolsDir(tool_dirs)) 271 272 # When using local vendor image, use cvd in local-tool if it 273 # exists. Fall back to downloaded tools in case it's missing 274 275 if avd_spec.local_vendor_image and avd_spec.local_tool_dirs: 276 try: 277 host_bins_path = self._FindCvdHostBinaries(tool_dirs) 278 except errors.GetCvdLocalHostPackageError: 279 logger.debug("fall back to downloaded cvd host binaries") 280 281 if avd_spec.local_system_image: 282 ( 283 system_image_path, 284 system_ext_image_path, 285 product_image_path, 286 ) = create_common.FindSystemImages(avd_spec.local_system_image) 287 288 if avd_spec.local_kernel_image: 289 ( 290 boot_image_path, 291 vendor_boot_image_path, 292 kernel_image_path, 293 initramfs_image_path, 294 ) = self.FindBootOrKernelImages( 295 os.path.abspath(avd_spec.local_kernel_image)) 296 297 if avd_spec.local_vendor_boot_image: 298 vendor_boot_image_path = create_common.FindVendorBootImage( 299 avd_spec.local_vendor_boot_image) 300 301 if avd_spec.local_vendor_image: 302 vendor_image_paths = cvd_utils.FindVendorImages( 303 avd_spec.local_vendor_image) 304 vendor_image_path = vendor_image_paths.vendor 305 vendor_dlkm_image_path = vendor_image_paths.vendor_dlkm 306 odm_image_path = vendor_image_paths.odm 307 odm_dlkm_image_path = vendor_image_paths.odm_dlkm 308 309 return local_image_local_instance.ArtifactPaths( 310 image_dir=mix_image_dir or image_dir, 311 host_bins=host_bins_path, 312 host_artifacts=image_dir, 313 misc_info=misc_info_path, 314 ota_tools_dir=ota_tools_dir, 315 system_image=system_image_path, 316 system_ext_image=system_ext_image_path, 317 product_image=product_image_path, 318 vendor_image=vendor_image_path, 319 vendor_dlkm_image=vendor_dlkm_image_path, 320 odm_image=odm_image_path, 321 odm_dlkm_image=odm_dlkm_image_path, 322 boot_image=boot_image_path, 323 vendor_boot_image=vendor_boot_image_path, 324 kernel_image=kernel_image_path, 325 initramfs_image=initramfs_image_path) 326