| # Copyright 2019 - 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. |
| |
| """RemoteInstanceDeviceFactory provides basic interface to create a cuttlefish |
| device factory.""" |
| |
| import glob |
| import logging |
| import os |
| import shutil |
| import tempfile |
| |
| from acloud import errors |
| from acloud.create import create_common |
| from acloud.internal import constants |
| from acloud.internal.lib import auth |
| from acloud.internal.lib import cvd_compute_client_multi_stage |
| from acloud.internal.lib import utils |
| from acloud.internal.lib import ssh |
| from acloud.public.actions import base_device_factory |
| |
| |
| logger = logging.getLogger(__name__) |
| |
| _USER_BUILD = "userbuild" |
| |
| |
| class RemoteInstanceDeviceFactory(base_device_factory.BaseDeviceFactory): |
| """A class that can produce a cuttlefish device. |
| |
| Attributes: |
| avd_spec: AVDSpec object that tells us what we're going to create. |
| cfg: An AcloudConfig instance. |
| local_image_artifact: A string, path to local image. |
| cvd_host_package_artifact: A string, path to cvd host package. |
| report_internal_ip: Boolean, True for the internal ip is used when |
| connecting from another GCE instance. |
| credentials: An oauth2client.OAuth2Credentials instance. |
| compute_client: An object of cvd_compute_client.CvdComputeClient. |
| ssh: An Ssh object. |
| """ |
| def __init__(self, avd_spec, local_image_artifact=None, |
| cvd_host_package_artifact=None): |
| """Constructs a new remote instance device factory.""" |
| self._avd_spec = avd_spec |
| self._cfg = avd_spec.cfg |
| self._local_image_artifact = local_image_artifact |
| self._cvd_host_package_artifact = cvd_host_package_artifact |
| self._report_internal_ip = avd_spec.report_internal_ip |
| self.credentials = auth.CreateCredentials(avd_spec.cfg) |
| # Control compute_client with enable_multi_stage |
| compute_client = cvd_compute_client_multi_stage.CvdComputeClient( |
| acloud_config=avd_spec.cfg, |
| oauth2_credentials=self.credentials, |
| ins_timeout_secs=avd_spec.ins_timeout_secs, |
| report_internal_ip=avd_spec.report_internal_ip, |
| gpu=avd_spec.gpu) |
| super(RemoteInstanceDeviceFactory, self).__init__(compute_client) |
| self._ssh = None |
| |
| def CreateInstance(self): |
| """Create a single configured cuttlefish device. |
| |
| GCE: |
| 1. Create gcp instance. |
| 2. Upload local built artifacts to remote instance or fetch build on |
| remote instance. |
| 3. Launch CVD. |
| |
| Remote host: |
| 1. Init remote host. |
| 2. Download the artifacts to local and upload the artifacts to host |
| 3. Launch CVD. |
| |
| Returns: |
| A string, representing instance name. |
| """ |
| if self._avd_spec.instance_type == constants.INSTANCE_TYPE_HOST: |
| instance = self._InitRemotehost() |
| self._ProcessRemoteHostArtifacts() |
| self._LaunchCvd(instance=instance, |
| decompress_kernel=True, |
| boot_timeout_secs=self._avd_spec.boot_timeout_secs) |
| else: |
| instance = self._CreateGceInstance() |
| # If instance is failed, no need to go next step. |
| if instance in self.GetFailures(): |
| return instance |
| try: |
| self._ProcessArtifacts(self._avd_spec.image_source) |
| self._LaunchCvd(instance=instance, |
| boot_timeout_secs=self._avd_spec.boot_timeout_secs) |
| except errors.DeviceConnectionError as e: |
| self._SetFailures(instance, e) |
| |
| return instance |
| |
| def _InitRemotehost(self): |
| """Initialize remote host. |
| |
| Determine the remote host instance name, and activate ssh. It need to |
| get the IP address in the common_operation. So need to pass the IP and |
| ssh to compute_client. |
| |
| build_target: The format is like "aosp_cf_x86_phone". We only get info |
| from the user build image file name. If the file name is |
| not custom format (no "-"), we will use $TARGET_PRODUCT |
| from environment variable as build_target. |
| |
| Returns: |
| A string, representing instance name. |
| """ |
| image_name = os.path.basename( |
| self._local_image_artifact) if self._local_image_artifact else "" |
| build_target = (os.environ.get(constants.ENV_BUILD_TARGET) if "-" not |
| in image_name else image_name.split("-")[0]) |
| build_id = _USER_BUILD |
| if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE: |
| build_id = self._avd_spec.remote_image[constants.BUILD_ID] |
| |
| instance = "%s-%s-%s-%s" % (constants.INSTANCE_TYPE_HOST, |
| self._avd_spec.remote_host, |
| build_id, build_target) |
| ip = ssh.IP(ip=self._avd_spec.remote_host) |
| self._ssh = ssh.Ssh( |
| ip=ip, |
| user=self._avd_spec.host_user, |
| ssh_private_key_path=(self._avd_spec.host_ssh_private_key_path or |
| self._cfg.ssh_private_key_path), |
| extra_args_ssh_tunnel=self._cfg.extra_args_ssh_tunnel, |
| report_internal_ip=self._report_internal_ip) |
| self._compute_client.InitRemoteHost( |
| self._ssh, ip, self._avd_spec.host_user) |
| return instance |
| |
| @utils.TimeExecute(function_description="Downloading Android Build artifact") |
| def _DownloadArtifacts(self, extract_path): |
| """Download the CF image artifacts and process them. |
| |
| - Download image from the Android Build system, then decompress it. |
| - Download cvd host package from the Android Build system. |
| |
| Args: |
| extract_path: String, a path include extracted files. |
| """ |
| cfg = self._avd_spec.cfg |
| build_id = self._avd_spec.remote_image[constants.BUILD_ID] |
| build_target = self._avd_spec.remote_image[constants.BUILD_TARGET] |
| |
| # Image zip |
| remote_image = "%s-img-%s.zip" % (build_target.split('-')[0], build_id) |
| create_common.DownloadRemoteArtifact( |
| cfg, build_target, build_id, remote_image, extract_path, decompress=True) |
| |
| # Cvd host package |
| create_common.DownloadRemoteArtifact( |
| cfg, build_target, build_id, constants.CVD_HOST_PACKAGE, |
| extract_path) |
| |
| def _ProcessRemoteHostArtifacts(self): |
| """Process remote host artifacts. |
| |
| - If images source is local, tool will upload images from local site to |
| remote host. |
| - If images source is remote, tool will download images from android |
| build to local and unzip it then upload to remote host, because there |
| is no permission to fetch build rom on the remote host. |
| """ |
| if self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL: |
| self._UploadArtifacts( |
| self._local_image_artifact, self._cvd_host_package_artifact, |
| self._avd_spec.local_image_dir) |
| else: |
| try: |
| artifacts_path = tempfile.mkdtemp() |
| logger.debug("Extracted path of artifacts: %s", artifacts_path) |
| self._DownloadArtifacts(artifacts_path) |
| self._UploadArtifacts( |
| None, |
| os.path.join(artifacts_path, constants.CVD_HOST_PACKAGE), |
| artifacts_path) |
| finally: |
| shutil.rmtree(artifacts_path) |
| |
| def _ProcessArtifacts(self, image_source): |
| """Process artifacts. |
| |
| - If images source is local, tool will upload images from local site to |
| remote instance. |
| - If images source is remote, tool will download images from android |
| build to remote instance. Before download images, we have to update |
| fetch_cvd to remote instance. |
| |
| Args: |
| image_source: String, the type of image source is remote or local. |
| """ |
| if image_source == constants.IMAGE_SRC_LOCAL: |
| self._UploadArtifacts(self._local_image_artifact, |
| self._cvd_host_package_artifact, |
| self._avd_spec.local_image_dir) |
| elif image_source == constants.IMAGE_SRC_REMOTE: |
| self._compute_client.UpdateFetchCvd() |
| self._FetchBuild( |
| self._avd_spec.remote_image[constants.BUILD_ID], |
| self._avd_spec.remote_image[constants.BUILD_BRANCH], |
| self._avd_spec.remote_image[constants.BUILD_TARGET], |
| self._avd_spec.system_build_info[constants.BUILD_ID], |
| self._avd_spec.system_build_info[constants.BUILD_BRANCH], |
| self._avd_spec.system_build_info[constants.BUILD_TARGET], |
| self._avd_spec.kernel_build_info[constants.BUILD_ID], |
| self._avd_spec.kernel_build_info[constants.BUILD_BRANCH], |
| self._avd_spec.kernel_build_info[constants.BUILD_TARGET]) |
| |
| def _FetchBuild(self, build_id, branch, build_target, system_build_id, |
| system_branch, system_build_target, kernel_build_id, |
| kernel_branch, kernel_build_target): |
| """Download CF artifacts from android build. |
| |
| Args: |
| build_branch: String, git branch name. e.g. "aosp-master" |
| build_target: String, the build target, e.g. cf_x86_phone-userdebug |
| build_id: String, build id, e.g. "2263051", "P2804227" |
| kernel_branch: Kernel branch name, e.g. "kernel-common-android-4.14" |
| kernel_build_id: Kernel build id, a string, e.g. "223051", "P280427" |
| kernel_build_target: String, Kernel build target name. |
| system_build_target: Target name for the system image, |
| e.g. "cf_x86_phone-userdebug" |
| system_branch: A String, branch name for the system image. |
| system_build_id: A string, build id for the system image. |
| |
| """ |
| self._compute_client.FetchBuild( |
| build_id, branch, build_target, system_build_id, |
| system_branch, system_build_target, kernel_build_id, |
| kernel_branch, kernel_build_target) |
| |
| def _CreateGceInstance(self): |
| """Create a single configured cuttlefish device. |
| |
| Override method from parent class. |
| build_target: The format is like "aosp_cf_x86_phone". We only get info |
| from the user build image file name. If the file name is |
| not custom format (no "-"), we will use $TARGET_PRODUCT |
| from environment variable as build_target. |
| |
| Returns: |
| A string, representing instance name. |
| """ |
| image_name = os.path.basename( |
| self._local_image_artifact) if self._local_image_artifact else "" |
| build_target = (os.environ.get(constants.ENV_BUILD_TARGET) if "-" not |
| in image_name else image_name.split("-")[0]) |
| build_id = _USER_BUILD |
| if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE: |
| build_id = self._avd_spec.remote_image[constants.BUILD_ID] |
| build_target = self._avd_spec.remote_image[constants.BUILD_TARGET] |
| |
| if self._avd_spec.instance_name_to_reuse: |
| instance = self._avd_spec.instance_name_to_reuse |
| else: |
| instance = self._compute_client.GenerateInstanceName( |
| build_target=build_target, build_id=build_id) |
| |
| # Create an instance from Stable Host Image |
| self._compute_client.CreateInstance( |
| instance=instance, |
| image_name=self._cfg.stable_host_image_name, |
| image_project=self._cfg.stable_host_image_project, |
| blank_data_disk_size_gb=self._cfg.extra_data_disk_size_gb, |
| avd_spec=self._avd_spec) |
| ip = self._compute_client.GetInstanceIP(instance) |
| self._ssh = ssh.Ssh(ip=ip, |
| user=constants.GCE_USER, |
| ssh_private_key_path=self._cfg.ssh_private_key_path, |
| extra_args_ssh_tunnel=self._cfg.extra_args_ssh_tunnel, |
| report_internal_ip=self._report_internal_ip) |
| return instance |
| |
| @utils.TimeExecute(function_description="Processing and uploading local images") |
| def _UploadArtifacts(self, |
| local_image_zip, |
| cvd_host_package_artifact, |
| images_dir): |
| """Upload local images and avd local host package to instance. |
| |
| There are two ways to upload local images. |
| 1. Using local image zip, it would be decompressed by install_zip.sh. |
| 2. Using local image directory, this directory contains all images. |
| Images are compressed/decompressed by lzop during upload process. |
| |
| Args: |
| local_image_zip: String, path to zip of local images which |
| build from 'm dist'. |
| cvd_host_package_artifact: String, path to cvd host package. |
| images_dir: String, directory of local images which build |
| from 'm'. |
| """ |
| if local_image_zip: |
| remote_cmd = ("/usr/bin/install_zip.sh . < %s" % local_image_zip) |
| logger.debug("remote_cmd:\n %s", remote_cmd) |
| self._ssh.Run(remote_cmd) |
| else: |
| # Compress image files for faster upload. |
| artifact_files = [os.path.basename(image) for image in glob.glob( |
| os.path.join(images_dir, "*.img"))] |
| cmd = ("tar -cf - --lzop -S -C {images_dir} {artifact_files} | " |
| "{ssh_cmd} -- tar -xf - --lzop -S".format( |
| images_dir=images_dir, |
| artifact_files=" ".join(artifact_files), |
| ssh_cmd=self._ssh.GetBaseCmd(constants.SSH_BIN))) |
| logger.debug("cmd:\n %s", cmd) |
| ssh.ShellCmdWithRetry(cmd) |
| |
| # host_package |
| remote_cmd = ("tar -x -z -f - < %s" % cvd_host_package_artifact) |
| logger.debug("remote_cmd:\n %s", remote_cmd) |
| self._ssh.Run(remote_cmd) |
| |
| def _LaunchCvd(self, instance, decompress_kernel=None, |
| boot_timeout_secs=None): |
| """Launch CVD. |
| |
| Args: |
| instance: String, instance name. |
| boot_timeout_secs: Integer, the maximum time to wait for the |
| command to respond. |
| """ |
| kernel_build = None |
| # TODO(b/140076771) Support kernel image for local image mode. |
| if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE: |
| kernel_build = self._compute_client.GetKernelBuild( |
| self._avd_spec.kernel_build_info[constants.BUILD_ID], |
| self._avd_spec.kernel_build_info[constants.BUILD_BRANCH], |
| self._avd_spec.kernel_build_info[constants.BUILD_TARGET]) |
| self._compute_client.LaunchCvd( |
| instance, |
| self._avd_spec, |
| self._cfg.extra_data_disk_size_gb, |
| kernel_build, |
| decompress_kernel, |
| boot_timeout_secs) |
| |
| def GetFailures(self): |
| """Get failures from all devices. |
| |
| Returns: |
| A dictionary that contains all the failures. |
| The key is the name of the instance that fails to boot, |
| and the value is an errors.DeviceBootError object. |
| """ |
| return self._compute_client.all_failures |
| |
| def _SetFailures(self, instance, error_msg): |
| """Set failures from this device. |
| |
| Record the failures for any steps in AVD creation. |
| |
| Args: |
| instance: String of instance name. |
| error_msg: String of error message. |
| """ |
| self._compute_client.all_failures[instance] = error_msg |
| |
| def GetBuildInfoDict(self): |
| """Get build info dictionary. |
| |
| Returns: |
| A build info dictionary. None for local image case. |
| """ |
| if self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL: |
| return None |
| build_info_dict = { |
| key: val for key, val in self._avd_spec.remote_image.items() if val} |
| |
| # kernel_target have default value "kernel". If user provide kernel_build_id |
| # or kernel_branch, then start to process kernel image. |
| if (self._avd_spec.kernel_build_info[constants.BUILD_ID] |
| or self._avd_spec.kernel_build_info[constants.BUILD_BRANCH]): |
| build_info_dict.update( |
| {"kernel_%s" % key: val |
| for key, val in self._avd_spec.kernel_build_info.items() if val} |
| ) |
| build_info_dict.update( |
| {"system_%s" % key: val |
| for key, val in self._avd_spec.system_build_info.items() if val} |
| ) |
| return build_info_dict |