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.
294 lines
12 KiB
294 lines
12 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"""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
|
|
|