blob: e507fb43c4e47649c82ac10bb26ef3718b2e7f36 [file] [log] [blame]
#!/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"""LocalImageRemoteInstance class.
Create class that is responsible for creating a remote instance AVD with a
local image.
"""
from distutils.spawn import find_executable
import getpass
import logging
import os
import subprocess
from acloud import errors
from acloud.create import base_avd_create
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
from acloud.internal.lib import utils
from acloud.public.actions import base_device_factory
from acloud.public.actions import common_operations
logger = logging.getLogger(__name__)
_CVD_HOST_PACKAGE = "cvd-host_package.tar.gz"
_CVD_USER = getpass.getuser()
_CMD_LAUNCH_CVD_ARGS = (" -cpus %s -x_res %s -y_res %s -dpi %s "
"-memory_mb %s -blank_data_image_mb %s "
"-data_policy always_create ")
#Output to Serial port 1 (console) group in the instance
_OUTPUT_CONSOLE_GROUPS = "tty"
SSH_BIN = "ssh"
_SSH_CMD = (" -i %(rsa_key_file)s "
"-q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "
"-l %(login_user)s %(ip_addr)s ")
_SSH_CMD_MAX_RETRY = 2
_SSH_CMD_RETRY_SLEEP = 3
_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.
image_path: A string, upload image artifact to instance.
cvd_host_package: A string, upload host package artifact to instance.
credentials: An oauth2client.OAuth2Credentials instance.
compute_client: An object of cvd_compute_client.CvdComputeClient.
"""
def __init__(self, avd_spec, local_image_artifact, cvd_host_package_artifact):
"""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)
compute_client = cvd_compute_client.CvdComputeClient(
avd_spec.cfg, self.credentials)
super(RemoteInstanceDeviceFactory, self).__init__(compute_client)
# Private creation parameters
self._ssh_cmd = None
def CreateInstance(self):
"""Create a single configured cuttlefish device.
1. Create gcp instance.
2. setup the AVD env in the instance.
3. upload the artifacts to instance.
4. Launch CVD.
Returns:
A string, representing instance name.
"""
instance = self._CreateGceInstance()
self._SetAVDenv(_CVD_USER)
self._UploadArtifacts(_CVD_USER,
self._local_image_artifact,
self._cvd_host_package_artifact)
self._LaunchCvd(_CVD_USER, self._avd_spec.hw_property)
return instance
@staticmethod
def _ShellCmdWithRetry(remote_cmd):
"""Runs a shell command on remote device.
If the network is unstable and causes SSH connect fail, it will retry.
When it retry in a short time, you may encounter unstable network. We
will use the mechanism of RETRY_BACKOFF_FACTOR. The retry time for each
failure is times * retries.
Args:
remote_cmd: A string, shell command to be run on remote.
Raises:
subprocess.CalledProcessError: For any non-zero return code of
remote_cmd.
Returns:
Boolean, True if the command was successfully executed. False otherwise.
"""
return utils.RetryExceptionType(
exception_types=subprocess.CalledProcessError,
max_retries=_SSH_CMD_MAX_RETRY,
functor=lambda cmd: subprocess.check_call(cmd, shell=True),
sleep_multiplier=_SSH_CMD_RETRY_SLEEP,
retry_backoff_factor=utils.DEFAULT_RETRY_BACKOFF_FACTOR,
cmd=remote_cmd)
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 the original
flavor as our build_target.
Returns:
A string, representing instance name.
"""
image_name = os.path.basename(self._local_image_artifact)
build_target = self._avd_spec.flavor if "-" not in image_name else image_name.split(
"-")[0]
instance = self._compute_client.GenerateInstanceName(
build_target=build_target, build_id=_USER_BUILD)
# 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_cmd = find_executable(SSH_BIN) + _SSH_CMD % {
"login_user": getpass.getuser(),
"rsa_key_file": self._cfg.ssh_private_key_path,
"ip_addr": (ip.internal if self._report_internal_ip
else ip.external)}
return instance
@utils.TimeExecute(function_description="Setting up GCE environment")
def _SetAVDenv(self, cvd_user):
"""set the user to run AVD in the instance.
Args:
cvd_user: A string, user run the cvd in the instance.
"""
avd_list_of_groups = []
avd_list_of_groups.extend(constants.LIST_CF_USER_GROUPS)
avd_list_of_groups.append(_OUTPUT_CONSOLE_GROUPS)
remote_cmd = ""
for group in avd_list_of_groups:
remote_cmd += "\"sudo usermod -aG %s %s;\"" %(group, cvd_user)
logger.debug("remote_cmd:\n %s", remote_cmd)
self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd)
@utils.TimeExecute(function_description="Uploading local image")
def _UploadArtifacts(self,
cvd_user,
local_image_artifact,
cvd_host_package_artifact):
"""Upload local image and avd local host package to instance.
Args:
cvd_user: A string, user upload the artifacts to instance.
local_image_artifact: A string, path to local image.
cvd_host_package_artifact: A string, path to cvd host package.
"""
# TODO(b/129376163) Use lzop for fast sparse image upload
remote_cmd = ("\"sudo su -c '/usr/bin/install_zip.sh .' - '%s'\" < %s" %
(cvd_user, local_image_artifact))
logger.debug("remote_cmd:\n %s", remote_cmd)
self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd)
# host_package
remote_cmd = ("\"sudo su -c 'tar -x -z -f -' - '%s'\" < %s" %
(cvd_user, cvd_host_package_artifact))
logger.debug("remote_cmd:\n %s", remote_cmd)
self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd)
def _LaunchCvd(self, cvd_user, hw_property):
"""Launch CVD."""
lunch_cvd_args = _CMD_LAUNCH_CVD_ARGS % (
hw_property["cpu"],
hw_property["x_res"],
hw_property["y_res"],
hw_property["dpi"],
hw_property["memory"],
hw_property["disk"])
remote_cmd = ("\"sudo su -c 'bin/launch_cvd %s>&/dev/ttyS0&' - '%s'\"" %
(lunch_cvd_args, cvd_user))
logger.debug("remote_cmd:\n %s", remote_cmd)
subprocess.Popen(self._ssh_cmd + remote_cmd, shell=True)
class LocalImageRemoteInstance(base_avd_create.BaseAVDCreate):
"""Create class for a local image remote instance AVD.
Attributes:
local_image_artifact: A string, path to local image.
cvd_host_package_artifact: A string, path to cvd host package.
"""
def __init__(self):
"""LocalImageRemoteInstance initialize."""
self.cvd_host_package_artifact = None
def VerifyHostPackageArtifactsExist(self):
"""Verify the host package exists and return its path.
Look for the host package in $ANDROID_HOST_OUT and dist dir.
Return:
A string, the path to the host package.
"""
dirs_to_check = filter(None,
[os.environ.get(constants.ENV_ANDROID_HOST_OUT)])
dist_dir = utils.GetDistDir()
if dist_dir:
dirs_to_check.append(dist_dir)
cvd_host_package_artifact = self.GetCvdHostPackage(dirs_to_check)
logger.debug("cvd host package: %s", cvd_host_package_artifact)
return cvd_host_package_artifact
@staticmethod
def GetCvdHostPackage(paths):
"""Get cvd host package path.
Args:
paths: A list, holds the paths to check for the host package.
Returns:
String, full path of cvd host package.
Raises:
errors.GetCvdLocalHostPackageError: Can't find cvd host package.
"""
for path in paths:
cvd_host_package = os.path.join(path, _CVD_HOST_PACKAGE)
if os.path.exists(cvd_host_package):
return cvd_host_package
raise errors.GetCvdLocalHostPackageError, (
"Can't find the cvd host package (Try lunching a cuttlefish target"
" like aosp_cf_x86_phone-userdebug and running 'm'): \n%s" %
'\n'.join(paths))
@utils.TimeExecute(function_description="Total time: ",
print_before_call=False, print_status=False)
def _CreateAVD(self, avd_spec, no_prompts):
"""Create the AVD.
Args:
avd_spec: AVDSpec object that tells us what we're going to create.
no_prompts: Boolean, True to skip all prompts.
"""
self.cvd_host_package_artifact = self.VerifyHostPackageArtifactsExist()
if avd_spec.local_image_artifact:
local_image_artifact = avd_spec.local_image_artifact
else:
local_image_artifact = create_common.ZipCFImageFiles(
avd_spec.local_image_dir)
device_factory = RemoteInstanceDeviceFactory(
avd_spec,
local_image_artifact,
self.cvd_host_package_artifact)
report = common_operations.CreateDevices(
"create_cf", avd_spec.cfg, device_factory, avd_spec.num,
report_internal_ip=avd_spec.report_internal_ip,
autoconnect=avd_spec.autoconnect,
avd_type=constants.TYPE_CF)
# Launch vnc client if we're auto-connecting.
if avd_spec.autoconnect:
utils.LaunchVNCFromReport(report, avd_spec, no_prompts)
return report