# 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

# 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

# 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. 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. Constructs a new remote instance device factory. # Control compute_client with enable_multi_stage 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 instance is failed, no need to go next step. 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. 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. # Image zip # Cvd host package 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. 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. 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. 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. # Create an instance from Stable Host Image 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'. # Compress image files for faster upload. # host_package Launch CVD.          Args:             instance: String, instance name.             boot_timeout_secs: Integer, the maximum time to wait for the                                command to respond. # TODO(b/140076771) Support kernel image for local image mode. 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. 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. Get build info dictionary.          Returns:             A build info dictionary. None for local image case. # kernel_target have default value "kernel". If user provide kernel_build_id # or kernel_branch, then start to process kernel image.