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 sys
24
25from acloud import errors
26from acloud.create import create_common
27from acloud.create import local_image_local_instance
28from acloud.internal import constants
29from acloud.internal.lib import utils
30from acloud.setup import setup_common
31
32
33logger = logging.getLogger(__name__)
34
35# Download remote image variables.
36_CUTTLEFISH_COMMON_BIN_PATH = "/usr/lib/cuttlefish-common/bin/"
37_CONFIRM_DOWNLOAD_DIR = ("Download dir %(download_dir)s does not have enough "
38                         "space (available space %(available_space)sGB, "
39                         "require %(required_space)sGB).\nPlease enter "
40                         "alternate path or 'q' to exit: ")
41# The downloaded image artifacts will take up ~8G:
42#   $du -lh --time $ANDROID_PRODUCT_OUT/aosp_cf_x86_phone-img-eng.XXX.zip
43#   422M
44# And decompressed becomes 7.2G (as of 11/2018).
45# Let's add an extra buffer (~2G) to make sure user has enough disk space
46# for the downloaded image artifacts.
47_REQUIRED_SPACE = 10
48
49
50@utils.TimeExecute(function_description="Downloading Android Build image")
51def DownloadAndProcessImageFiles(avd_spec):
52    """Download the CF image artifacts and process them.
53
54    It will download two artifacts and process them in this function. One is
55    cvd_host_package.tar.gz, the other is rom image zip. If the build_id is
56    "1234" and build_target is "aosp_cf_x86_phone-userdebug",
57    the image zip name is "aosp_cf_x86_phone-img-1234.zip".
58
59    Args:
60        avd_spec: AVDSpec object that tells us what we're going to create.
61
62    Returns:
63        extract_path: String, path to image folder.
64    """
65    cfg = avd_spec.cfg
66    build_id = avd_spec.remote_image[constants.BUILD_ID]
67    build_target = avd_spec.remote_image[constants.BUILD_TARGET]
68
69    extract_path = os.path.join(
70        avd_spec.image_download_dir,
71        constants.TEMP_ARTIFACTS_FOLDER,
72        build_id)
73
74    logger.debug("Extract path: %s", extract_path)
75    # TODO(b/117189191): If extract folder exists, check if the files are
76    # already downloaded and skip this step if they are.
77    if not os.path.exists(extract_path):
78        os.makedirs(extract_path)
79        remote_image = "%s-img-%s.zip" % (build_target.split('-')[0],
80                                          build_id)
81        artifacts = [constants.CVD_HOST_PACKAGE, remote_image]
82        for artifact in artifacts:
83            create_common.DownloadRemoteArtifact(
84                cfg, build_target, build_id, artifact, extract_path, decompress=True)
85    return extract_path
86
87
88def ConfirmDownloadRemoteImageDir(download_dir):
89    """Confirm download remote image directory.
90
91    If available space of download_dir is less than _REQUIRED_SPACE, ask
92    the user to choose a different download dir or to exit out since acloud will
93    fail to download the artifacts due to insufficient disk space.
94
95    Args:
96        download_dir: String, a directory for download and decompress.
97
98    Returns:
99        String, Specific download directory when user confirm to change.
100    """
101    while True:
102        download_dir = os.path.expanduser(download_dir)
103        if not os.path.exists(download_dir):
104            answer = utils.InteractWithQuestion(
105                "No such directory %s.\nEnter 'y' to create it, enter "
106                "anything else to exit out[y/N]: " % download_dir)
107            if answer.lower() == "y":
108                os.makedirs(download_dir)
109            else:
110                sys.exit(constants.EXIT_BY_USER)
111
112        stat = os.statvfs(download_dir)
113        available_space = stat.f_bavail*stat.f_bsize/(1024)**3
114        if available_space < _REQUIRED_SPACE:
115            download_dir = utils.InteractWithQuestion(
116                _CONFIRM_DOWNLOAD_DIR % {"download_dir":download_dir,
117                                         "available_space":available_space,
118                                         "required_space":_REQUIRED_SPACE})
119            if download_dir.lower() == "q":
120                sys.exit(constants.EXIT_BY_USER)
121        else:
122            return download_dir
123
124
125class RemoteImageLocalInstance(local_image_local_instance.LocalImageLocalInstance):
126    """Create class for a remote image local instance AVD.
127
128    RemoteImageLocalInstance just defines logic in downloading the remote image
129    artifacts and leverages the existing logic to launch a local instance in
130    LocalImageLocalInstance.
131    """
132
133    def GetImageArtifactsPath(self, avd_spec):
134        """Download the image artifacts and return the paths to them.
135
136        Args:
137            avd_spec: AVDSpec object that tells us what we're going to create.
138
139        Raises:
140            errors.NoCuttlefishCommonInstalled: cuttlefish-common doesn't install.
141
142        Returns:
143            Tuple of (local image file, host bins package) paths.
144        """
145        if not setup_common.PackageInstalled("cuttlefish-common"):
146            raise errors.NoCuttlefishCommonInstalled(
147                "Package [cuttlefish-common] is not installed!\n"
148                "Please run 'acloud setup --host' to install.")
149
150        avd_spec.image_download_dir = ConfirmDownloadRemoteImageDir(
151            avd_spec.image_download_dir)
152
153        image_dir = DownloadAndProcessImageFiles(avd_spec)
154        launch_cvd_path = os.path.join(image_dir, "bin",
155                                       constants.CMD_LAUNCH_CVD)
156        if not os.path.exists(launch_cvd_path):
157            raise errors.GetCvdLocalHostPackageError(
158                "No launch_cvd found. Please check downloaded artifacts dir: %s"
159                % image_dir)
160        return image_dir, image_dir
161