blob: 8935aecfa251066a6fec6b03794d5d5a526f9669 [file] [log] [blame]
# 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