#!/usr/bin/env python # # Copyright 2018 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. r"""RemoteImageLocalInstance class. Create class that is responsible for creating a local instance AVD with a remote image. """ from __future__ import print_function import glob import logging import os import subprocess import sys from acloud import errors from acloud.create import local_image_local_instance from acloud.internal import constants from acloud.internal.lib import android_build_client from acloud.internal.lib import auth from acloud.internal.lib import utils from acloud.setup import setup_common # Download remote image variables. _CVD_HOST_PACKAGE = "cvd-host_package.tar.gz" _CUTTLEFISH_COMMON_BIN_PATH = "/usr/lib/cuttlefish-common/bin/" _CONFIRM_DOWNLOAD_DIR = ("Download dir %(download_dir)s does not have enough " "space (available space %(available_space)sGB, " "require %(required_space)sGB).\nPlease enter " "alternate path or 'q' to exit: ") # The downloaded image artifacts will take up ~8G: # $du -lh --time $ANDROID_PRODUCT_OUT/aosp_cf_x86_phone-img-eng.XXX.zip # 422M # And decompressed becomes 7.2G (as of 11/2018). # Let's add an extra buffer (~2G) to make sure user has enough disk space # for the downloaded image artifacts. _REQUIRED_SPACE = 10 _BOOT_IMAGE = "boot.img" # TODO(b/129009852):UnpackBootImage and setfacl are deprecated. UNPACK_BOOTIMG_CMD = "%s -boot_img %s" % ( os.path.join(_CUTTLEFISH_COMMON_BIN_PATH, "unpack_boot_image.py"), "%s -dest %s") ACL_CMD = "setfacl -m g:libvirt-qemu:rw %s" logger = logging.getLogger(__name__) class RemoteImageLocalInstance(local_image_local_instance.LocalImageLocalInstance): """Create class for a remote image local instance AVD. RemoteImageLocalInstance just defines logic in downloading the remote image artifacts and leverages the existing logic to launch a local instance in LocalImageLocalInstance. """ def GetImageArtifactsPath(self, avd_spec): """Download the image artifacts and return the paths to them. Args: avd_spec: AVDSpec object that tells us what we're going to create. Raises: errors.NoCuttlefishCommonInstalled: cuttlefish-common doesn't install. Returns: Tuple of (local image file, host bins package) paths. """ if not setup_common.PackageInstalled("cuttlefish-common"): raise errors.NoCuttlefishCommonInstalled( "Package [cuttlefish-common] is not installed!\n" "Please run 'acloud setup --host' to install.") avd_spec.image_download_dir = self._ConfirmDownloadRemoteImageDir( avd_spec.image_download_dir) image_dir = self._DownloadAndProcessImageFiles(avd_spec) launch_cvd_path = os.path.join(image_dir, "bin", constants.CMD_LAUNCH_CVD) if not os.path.exists(launch_cvd_path): raise errors.GetCvdLocalHostPackageError( "No launch_cvd found. Please check downloaded artifacts dir: %s" % image_dir) return image_dir, image_dir @utils.TimeExecute(function_description="Downloading Android Build image") def _DownloadAndProcessImageFiles(self, avd_spec): """Download the CF image artifacts and process them. Download from the Android Build system, unpack the boot img file, and ACL the image files. Args: avd_spec: AVDSpec object that tells us what we're going to create. Returns: extract_path: String, path to image folder. """ cfg = avd_spec.cfg build_id = avd_spec.remote_image[constants.BUILD_ID] build_target = avd_spec.remote_image[constants.BUILD_TARGET] extract_path = os.path.join( avd_spec.image_download_dir, constants.TEMP_ARTIFACTS_FOLDER, build_id) logger.debug("Extract path: %s", extract_path) # TODO(b/117189191): If extract folder exists, check if the files are # already downloaded and skip this step if they are. if not os.path.exists(extract_path): os.makedirs(extract_path) self._DownloadRemoteImage(cfg, build_target, build_id, extract_path) self._UnpackBootImage(extract_path) self._AclCfImageFiles(extract_path) return extract_path @staticmethod def _DownloadRemoteImage(cfg, build_target, build_id, extract_path): """Download cuttlefish package and remote image then extract them. Args: cfg: An AcloudConfig instance. build_target: String, the build target, e.g. cf_x86_phone-userdebug. build_id: String, Build id, e.g. "2263051", "P2804227" extract_path: String, a path include extracted files. """ remote_image = "%s-img-%s.zip" % (build_target.split('-')[0], build_id) artifacts = [_CVD_HOST_PACKAGE, remote_image] build_client = android_build_client.AndroidBuildClient( auth.CreateCredentials(cfg)) for artifact in artifacts: temp_filename = os.path.join(extract_path, artifact) build_client.DownloadArtifact( build_target, build_id, artifact, temp_filename) utils.Decompress(temp_filename, extract_path) try: os.remove(temp_filename) logger.debug("Deleted temporary file %s", temp_filename) except OSError as e: logger.error("Failed to delete temporary file: %s", str(e)) @staticmethod def _UnpackBootImage(extract_path): """Unpack Boot.img. Args: extract_path: String, a path include extracted files. Raises: errors.BootImgDoesNotExist: boot.img doesn't exist. errors.UnpackBootImageError: Unpack boot.img fail. """ bootimg_path = os.path.join(extract_path, _BOOT_IMAGE) if not os.path.exists(bootimg_path): raise errors.BootImgDoesNotExist( "%s does not exist in %s" % (_BOOT_IMAGE, bootimg_path)) logger.info("Start to unpack boot.img.") try: subprocess.check_call( UNPACK_BOOTIMG_CMD % (bootimg_path, extract_path), shell=True) except subprocess.CalledProcessError as e: raise errors.UnpackBootImageError( "Failed to unpack boot.img: %s" % str(e)) logger.info("Unpack boot.img complete!") @staticmethod def _AclCfImageFiles(extract_path): """ACL related files. Use setfacl so that libvirt does not lose access to this file if user does anything to this file at any point. Args: extract_path: String, a path include extracted files. """ image_list = glob.glob(os.path.join(extract_path, "*.img")) logger.info("Start to set ACLs on files: %s", ",".join(image_list)) for image_path in image_list: subprocess.check_call(ACL_CMD % image_path, shell=True) logger.info("The ACLs have set completed!") @staticmethod def _ConfirmDownloadRemoteImageDir(download_dir): """Confirm download remote image directory. If available space of download_dir is less than _REQUIRED_SPACE, ask the user to choose a different d/l dir or to exit out since acloud will fail to download the artifacts due to insufficient disk space. Args: download_dir: String, a directory for download and decompress. Returns: String, Specific download directory when user confirm to change. """ while True: download_dir = os.path.expanduser(download_dir) if not os.path.exists(download_dir): answer = utils.InteractWithQuestion( "No such directory %s.\nEnter 'y' to create it, enter " "anything else to exit out[y/N]: " % download_dir) if answer.lower() == "y": os.makedirs(download_dir) else: sys.exit(constants.EXIT_BY_USER) stat = os.statvfs(download_dir) available_space = stat.f_bavail*stat.f_bsize/(1024)**3 if available_space < _REQUIRED_SPACE: download_dir = utils.InteractWithQuestion( _CONFIRM_DOWNLOAD_DIR % {"download_dir":download_dir, "available_space":available_space, "required_space":_REQUIRED_SPACE}) if download_dir.lower() == "q": sys.exit(constants.EXIT_BY_USER) else: return download_dir