普通文本  |  295行  |  11.6 KB

#!/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