You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
9.3 KiB
237 lines
9.3 KiB
#!/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
|
|
|