#!/usr/bin/env python
#
# Copyright 2016 - 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.
"""A client that manages Android compute engine instances.

** AndroidComputeClient **

AndroidComputeClient derives from ComputeClient. It manges a google
compute engine project that is setup for running Android instances.
It knows how to create android GCE images and instances.

** Class hierarchy **

  base_cloud_client.BaseCloudApiClient
                ^
                |
       gcompute_client.ComputeClient
                ^
                |
    gcompute_client.AndroidComputeClient
"""

import getpass
import logging
import os
import uuid

from acloud import errors
from acloud.internal import constants
from acloud.internal.lib import gcompute_client
from acloud.internal.lib import utils
from acloud.public import config


logger = logging.getLogger(__name__)
_ZONE = "zone"
_VERSION = "version"


class AndroidComputeClient(gcompute_client.ComputeClient):
    """Client that manages Anadroid Virtual Device."""
    IMAGE_NAME_FMT = "img-{uuid}-{build_id}-{build_target}"
    DATA_DISK_NAME_FMT = "data-{instance}"
    BOOT_COMPLETED_MSG = "VIRTUAL_DEVICE_BOOT_COMPLETED"
    BOOT_STARTED_MSG = "VIRTUAL_DEVICE_BOOT_STARTED"
    BOOT_CHECK_INTERVAL_SECS = 10
    OPERATION_TIMEOUT_SECS = 20 * 60  # Override parent value, 20 mins

    NAME_LENGTH_LIMIT = 63
    # If the generated name ends with '-', replace it with REPLACER.
    REPLACER = "e"

    def __init__(self, acloud_config, oauth2_credentials):
        """Initialize.

        Args:
            acloud_config: An AcloudConfig object.
            oauth2_credentials: An oauth2client.OAuth2Credentials instance.
        """
        super(AndroidComputeClient, self).__init__(acloud_config,
                                                   oauth2_credentials)
        self._zone = acloud_config.zone
        self._machine_type = acloud_config.machine_type
        self._min_machine_size = acloud_config.min_machine_size
        self._network = acloud_config.network
        self._orientation = acloud_config.orientation
        self._resolution = acloud_config.resolution
        self._metadata = acloud_config.metadata_variable.copy()
        self._ssh_public_key_path = acloud_config.ssh_public_key_path
        self._launch_args = acloud_config.launch_args
        self._instance_name_pattern = acloud_config.instance_name_pattern
        self._AddPerInstanceSshkey()
        self._dict_report = {_ZONE: self._zone,
                             _VERSION: config.GetVersion()}

    # TODO(147047953): New args to contorl zone metrics check.
    def _VerifyZoneByQuota(self):
        """Verify the zone must have enough quota to create instance.

        Returns:
            Boolean, True if zone have enough quota to create instance.

        Raises:
            errors.CheckGCEZonesQuotaError: the zone doesn't have enough quota.
        """
        if self.EnoughMetricsInZone(self._zone):
            return True
        raise errors.CheckGCEZonesQuotaError(
            "There is no enough quota in zone: %s" % self._zone)

    def _AddPerInstanceSshkey(self):
        """Add per-instance ssh key.

        Assign the ssh publick key to instacne then use ssh command to
        control remote instance via the ssh publick key. Added sshkey for two
        users. One is vsoc01, another is current user.

        """
        if self._ssh_public_key_path:
            rsa = self._LoadSshPublicKey(self._ssh_public_key_path)
            logger.info("ssh_public_key_path is specified in config: %s, "
                        "will add the key to the instance.",
                        self._ssh_public_key_path)
            self._metadata["sshKeys"] = "{0}:{2}\n{1}:{2}".format(getpass.getuser(),
                                                                  constants.GCE_USER,
                                                                  rsa)
        else:
            logger.warning(
                "ssh_public_key_path is not specified in config, "
                "only project-wide key will be effective.")

    @classmethod
    def _FormalizeName(cls, name):
        """Formalize the name to comply with RFC1035.

        The name must be 1-63 characters long and match the regular expression
        [a-z]([-a-z0-9]*[a-z0-9])? which means the first character must be a
        lowercase letter, and all following characters must be a dash,
        lowercase letter, or digit, except the last character, which cannot be
        a dash.

        Args:
          name: A string.

        Returns:
          name: A string that complies with RFC1035.
        """
        name = name.replace("_", "-").lower()
        name = name[:cls.NAME_LENGTH_LIMIT]
        if name[-1] == "-":
            name = name[:-1] + cls.REPLACER
        return name

    def _CheckMachineSize(self):
        """Check machine size.

        Check if the desired machine type |self._machine_type| meets
        the requirement of minimum machine size specified as
        |self._min_machine_size|.

        Raises:
            errors.DriverError: if check fails.
        """
        if self.CompareMachineSize(self._machine_type, self._min_machine_size,
                                   self._zone) < 0:
            raise errors.DriverError(
                "%s does not meet the minimum required machine size %s" %
                (self._machine_type, self._min_machine_size))

    @classmethod
    def GenerateImageName(cls, build_target=None, build_id=None):
        """Generate an image name given build_target, build_id.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing image name.
        """
        if not build_target and not build_id:
            return "image-" + uuid.uuid4().hex
        name = cls.IMAGE_NAME_FMT.format(
            build_target=build_target,
            build_id=build_id,
            uuid=uuid.uuid4().hex[:8])
        return cls._FormalizeName(name)

    @classmethod
    def GetDataDiskName(cls, instance):
        """Get data disk name for an instance.

        Args:
            instance: An instance_name.

        Returns:
            The corresponding data disk name.
        """
        name = cls.DATA_DISK_NAME_FMT.format(instance=instance)
        return cls._FormalizeName(name)

    def GenerateInstanceName(self, build_target=None, build_id=None):
        """Generate an instance name given build_target, build_id.

        Target is not used as instance name has a length limit.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing instance name.
        """
        name = self._instance_name_pattern.format(build_target=build_target,
                                                  build_id=build_id,
                                                  uuid=uuid.uuid4().hex[:8])
        return self._FormalizeName(name)

    def CreateDisk(self,
                   disk_name,
                   source_image,
                   size_gb,
                   zone=None,
                   source_project=None,
                   disk_type=gcompute_client.PersistentDiskType.STANDARD):
        """Create a gce disk.

        Args:
            disk_name: String, name of disk.
            source_image: String, name to the image name.
            size_gb: Integer, size in gigabytes.
            zone: String, name of the zone, e.g. us-central1-b.
            source_project: String, required if the image is located in a different
                            project.
            disk_type: String, a value from PersistentDiskType, STANDARD
                       for regular hard disk or SSD for solid state disk.
        """
        if self.CheckDiskExists(disk_name, self._zone):
            raise errors.DriverError(
                "Failed to create disk %s, already exists." % disk_name)
        if source_image and not self.CheckImageExists(source_image):
            raise errors.DriverError(
                "Failed to create disk %s, source image %s does not exist." %
                (disk_name, source_image))
        super(AndroidComputeClient, self).CreateDisk(
            disk_name,
            source_image=source_image,
            size_gb=size_gb,
            zone=zone or self._zone)

    @staticmethod
    def _LoadSshPublicKey(ssh_public_key_path):
        """Load the content of ssh public key from a file.

        Args:
            ssh_public_key_path: String, path to the public key file.
                               E.g. ~/.ssh/acloud_rsa.pub
        Returns:
            String, content of the file.

        Raises:
            errors.DriverError if the public key file does not exist
            or the content is not valid.
        """
        key_path = os.path.expanduser(ssh_public_key_path)
        if not os.path.exists(key_path):
            raise errors.DriverError(
                "SSH public key file %s does not exist." % key_path)

        with open(key_path) as f:
            rsa = f.read()
            rsa = rsa.strip() if rsa else rsa
            utils.VerifyRsaPubKey(rsa)
        return rsa

    # pylint: disable=too-many-locals, arguments-differ
    @utils.TimeExecute("Creating GCE Instance")
    def CreateInstance(self,
                       instance,
                       image_name,
                       machine_type=None,
                       metadata=None,
                       network=None,
                       zone=None,
                       disk_args=None,
                       image_project=None,
                       gpu=None,
                       extra_disk_name=None,
                       avd_spec=None,
                       extra_scopes=None,
                       tags=None):
        """Create a gce instance with a gce image.

        Args:
            instance: String, instance name.
            image_name: String, source image used to create this disk.
            machine_type: String, representing machine_type,
                          e.g. "n1-standard-1"
            metadata: Dict, maps a metadata name to its value.
            network: String, representing network name, e.g. "default"
            zone: String, representing zone name, e.g. "us-central1-f"
            disk_args: A list of extra disk args (strings), see _GetDiskArgs
                       for example, if None, will create a disk using the given
                       image.
            image_project: String, name of the project where the image
                           belongs. Assume the default project if None.
            gpu: String, type of gpu to attach. e.g. "nvidia-tesla-k80", if
                 None no gpus will be attached. For more details see:
                 https://cloud.google.com/compute/docs/gpus/add-gpus
            extra_disk_name: String,the name of the extra disk to attach.
            avd_spec: AVDSpec object that tells us what we're going to create.
            extra_scopes: List, extra scopes (strings) to be passed to the
                          instance.
            tags: A list of tags to associate with the instance. e.g.
                 ["http-server", "https-server"]
        """
        self._CheckMachineSize()
        disk_args = self._GetDiskArgs(instance, image_name)
        metadata = self._metadata.copy()
        metadata["cfg_sta_display_resolution"] = self._resolution
        metadata["t_force_orientation"] = self._orientation
        metadata[constants.INS_KEY_AVD_TYPE] = avd_spec.avd_type

        # Use another METADATA_DISPLAY to record resolution which will be
        # retrieved in acloud list cmd. We try not to use cvd_01_x_res
        # since cvd_01_xxx metadata is going to deprecated by cuttlefish.
        metadata[constants.INS_KEY_DISPLAY] = ("%sx%s (%s)" % (
            avd_spec.hw_property[constants.HW_X_RES],
            avd_spec.hw_property[constants.HW_Y_RES],
            avd_spec.hw_property[constants.HW_ALIAS_DPI]))

        super(AndroidComputeClient, self).CreateInstance(
            instance, image_name, self._machine_type, metadata, self._network,
            self._zone, disk_args, image_project, gpu, extra_disk_name,
            extra_scopes=extra_scopes, tags=tags)

    def CheckBootFailure(self, serial_out, instance):
        """Determine if serial output has indicated any boot failure.

        Subclass has to define this function to detect failures
        in the boot process

        Args:
            serial_out: string
            instance: string, instance name.

        Raises:
            Raises errors.DeviceBootError exception if a failure is detected.
        """
        pass

    def CheckBoot(self, instance):
        """Check once to see if boot completes.

        Args:
            instance: string, instance name.

        Returns:
            True if the BOOT_COMPLETED_MSG or BOOT_STARTED_MSG appears in serial
            port output, otherwise False.
        """
        try:
            serial_out = self.GetSerialPortOutput(instance=instance, port=1)
            self.CheckBootFailure(serial_out, instance)
            return ((self.BOOT_COMPLETED_MSG in serial_out)
                    or (self.BOOT_STARTED_MSG in serial_out))
        except errors.HttpError as e:
            if e.code == 400:
                logger.debug("CheckBoot: Instance is not ready yet %s", str(e))
                return False
            raise

    def WaitForBoot(self, instance, boot_timeout_secs=None):
        """Wait for boot to completes or hit timeout.

        Args:
            instance: string, instance name.
            boot_timeout_secs: Integer, the maximum time in seconds used to
                               wait for the AVD to boot.
        """
        boot_timeout_secs = boot_timeout_secs or constants.DEFAULT_CF_BOOT_TIMEOUT
        logger.info("Waiting for instance to boot up %s for %s secs",
                    instance, boot_timeout_secs)
        timeout_exception = errors.DeviceBootTimeoutError(
            "Device %s did not finish on boot within timeout (%s secs)" %
            (instance, boot_timeout_secs))
        utils.PollAndWait(
            func=self.CheckBoot,
            expected_return=True,
            timeout_exception=timeout_exception,
            timeout_secs=boot_timeout_secs,
            sleep_interval_secs=self.BOOT_CHECK_INTERVAL_SECS,
            instance=instance)
        logger.info("Instance boot completed: %s", instance)

    def GetInstanceIP(self, instance, zone=None):
        """Get Instance IP given instance name.

        Args:
            instance: String, representing instance name.
            zone: String, representing zone name, e.g. "us-central1-f"

        Returns:
            ssh.IP object, that stores internal and external ip of the instance.
        """
        return super(AndroidComputeClient, self).GetInstanceIP(
            instance, zone or self._zone)

    def GetSerialPortOutput(self, instance, zone=None, port=1):
        """Get serial port output.

        Args:
            instance: string, instance name.
            zone: String, representing zone name, e.g. "us-central1-f"
            port: int, which COM port to read from, 1-4, default to 1.

        Returns:
            String, contents of the output.

        Raises:
            errors.DriverError: For malformed response.
        """
        return super(AndroidComputeClient, self).GetSerialPortOutput(
            instance, zone or self._zone, port)

    def ExtendReportData(self, key, value):
        """Extend the report data.

        Args:
            key: string of key name.
            value: string of data value.
        """
        self._dict_report.update({key: value})

    @property
    def dict_report(self):
        """Return dict_report"""
        return self._dict_report

f77e2aad017ee9562028335a5ab97d28a085da6e
<mask token>


class AndroidComputeClient(gcompute_client.ComputeClient):
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>

    def __init__(self, acloud_config, oauth2_credentials):
        """Initialize.

        Args:
            acloud_config: An AcloudConfig object.
            oauth2_credentials: An oauth2client.OAuth2Credentials instance.
        """
        super(AndroidComputeClient, self).__init__(acloud_config,
            oauth2_credentials)
        self._zone = acloud_config.zone
        self._machine_type = acloud_config.machine_type
        self._min_machine_size = acloud_config.min_machine_size
        self._network = acloud_config.network
        self._orientation = acloud_config.orientation
        self._resolution = acloud_config.resolution
        self._metadata = acloud_config.metadata_variable.copy()
        self._ssh_public_key_path = acloud_config.ssh_public_key_path
        self._launch_args = acloud_config.launch_args
        self._instance_name_pattern = acloud_config.instance_name_pattern
        self._AddPerInstanceSshkey()
        self._dict_report = {_ZONE: self._zone, _VERSION: config.GetVersion()}

    def _VerifyZoneByQuota(self):
        """Verify the zone must have enough quota to create instance.

        Returns:
            Boolean, True if zone have enough quota to create instance.

        Raises:
            errors.CheckGCEZonesQuotaError: the zone doesn't have enough quota.
        """
        if self.EnoughMetricsInZone(self._zone):
            return True
        raise errors.CheckGCEZonesQuotaError(
            'There is no enough quota in zone: %s' % self._zone)

    def _AddPerInstanceSshkey(self):
        """Add per-instance ssh key.

        Assign the ssh publick key to instacne then use ssh command to
        control remote instance via the ssh publick key. Added sshkey for two
        users. One is vsoc01, another is current user.

        """
        if self._ssh_public_key_path:
            rsa = self._LoadSshPublicKey(self._ssh_public_key_path)
            logger.info(
                'ssh_public_key_path is specified in config: %s, will add the key to the instance.'
                , self._ssh_public_key_path)
            self._metadata['sshKeys'] = '{0}:{2}\n{1}:{2}'.format(getpass.
                getuser(), constants.GCE_USER, rsa)
        else:
            logger.warning(
                'ssh_public_key_path is not specified in config, only project-wide key will be effective.'
                )
    <mask token>

    def _CheckMachineSize(self):
        """Check machine size.

        Check if the desired machine type |self._machine_type| meets
        the requirement of minimum machine size specified as
        |self._min_machine_size|.

        Raises:
            errors.DriverError: if check fails.
        """
        if self.CompareMachineSize(self._machine_type, self.
            _min_machine_size, self._zone) < 0:
            raise errors.DriverError(
                '%s does not meet the minimum required machine size %s' % (
                self._machine_type, self._min_machine_size))

    @classmethod
    def GenerateImageName(cls, build_target=None, build_id=None):
        """Generate an image name given build_target, build_id.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing image name.
        """
        if not build_target and not build_id:
            return 'image-' + uuid.uuid4().hex
        name = cls.IMAGE_NAME_FMT.format(build_target=build_target,
            build_id=build_id, uuid=uuid.uuid4().hex[:8])
        return cls._FormalizeName(name)
    <mask token>

    def GenerateInstanceName(self, build_target=None, build_id=None):
        """Generate an instance name given build_target, build_id.

        Target is not used as instance name has a length limit.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing instance name.
        """
        name = self._instance_name_pattern.format(build_target=build_target,
            build_id=build_id, uuid=uuid.uuid4().hex[:8])
        return self._FormalizeName(name)
    <mask token>

    @staticmethod
    def _LoadSshPublicKey(ssh_public_key_path):
        """Load the content of ssh public key from a file.

        Args:
            ssh_public_key_path: String, path to the public key file.
                               E.g. ~/.ssh/acloud_rsa.pub
        Returns:
            String, content of the file.

        Raises:
            errors.DriverError if the public key file does not exist
            or the content is not valid.
        """
        key_path = os.path.expanduser(ssh_public_key_path)
        if not os.path.exists(key_path):
            raise errors.DriverError(
                'SSH public key file %s does not exist.' % key_path)
        with open(key_path) as f:
            rsa = f.read()
            rsa = rsa.strip() if rsa else rsa
            utils.VerifyRsaPubKey(rsa)
        return rsa

    @utils.TimeExecute('Creating GCE Instance')
    def CreateInstance(self, instance, image_name, machine_type=None,
        metadata=None, network=None, zone=None, disk_args=None,
        image_project=None, gpu=None, extra_disk_name=None, avd_spec=None,
        extra_scopes=None, tags=None):
        """Create a gce instance with a gce image.

        Args:
            instance: String, instance name.
            image_name: String, source image used to create this disk.
            machine_type: String, representing machine_type,
                          e.g. "n1-standard-1"
            metadata: Dict, maps a metadata name to its value.
            network: String, representing network name, e.g. "default"
            zone: String, representing zone name, e.g. "us-central1-f"
            disk_args: A list of extra disk args (strings), see _GetDiskArgs
                       for example, if None, will create a disk using the given
                       image.
            image_project: String, name of the project where the image
                           belongs. Assume the default project if None.
            gpu: String, type of gpu to attach. e.g. "nvidia-tesla-k80", if
                 None no gpus will be attached. For more details see:
                 https://cloud.google.com/compute/docs/gpus/add-gpus
            extra_disk_name: String,the name of the extra disk to attach.
            avd_spec: AVDSpec object that tells us what we're going to create.
            extra_scopes: List, extra scopes (strings) to be passed to the
                          instance.
            tags: A list of tags to associate with the instance. e.g.
                 ["http-server", "https-server"]
        """
        self._CheckMachineSize()
        disk_args = self._GetDiskArgs(instance, image_name)
        metadata = self._metadata.copy()
        metadata['cfg_sta_display_resolution'] = self._resolution
        metadata['t_force_orientation'] = self._orientation
        metadata[constants.INS_KEY_AVD_TYPE] = avd_spec.avd_type
        metadata[constants.INS_KEY_DISPLAY] = '%sx%s (%s)' % (avd_spec.
            hw_property[constants.HW_X_RES], avd_spec.hw_property[constants
            .HW_Y_RES], avd_spec.hw_property[constants.HW_ALIAS_DPI])
        super(AndroidComputeClient, self).CreateInstance(instance,
            image_name, self._machine_type, metadata, self._network, self.
            _zone, disk_args, image_project, gpu, extra_disk_name,
            extra_scopes=extra_scopes, tags=tags)
    <mask token>

    def CheckBoot(self, instance):
        """Check once to see if boot completes.

        Args:
            instance: string, instance name.

        Returns:
            True if the BOOT_COMPLETED_MSG or BOOT_STARTED_MSG appears in serial
            port output, otherwise False.
        """
        try:
            serial_out = self.GetSerialPortOutput(instance=instance, port=1)
            self.CheckBootFailure(serial_out, instance)
            return (self.BOOT_COMPLETED_MSG in serial_out or self.
                BOOT_STARTED_MSG in serial_out)
        except errors.HttpError as e:
            if e.code == 400:
                logger.debug('CheckBoot: Instance is not ready yet %s', str(e))
                return False
            raise
    <mask token>

    def GetInstanceIP(self, instance, zone=None):
        """Get Instance IP given instance name.

        Args:
            instance: String, representing instance name.
            zone: String, representing zone name, e.g. "us-central1-f"

        Returns:
            ssh.IP object, that stores internal and external ip of the instance.
        """
        return super(AndroidComputeClient, self).GetInstanceIP(instance, 
            zone or self._zone)

    def GetSerialPortOutput(self, instance, zone=None, port=1):
        """Get serial port output.

        Args:
            instance: string, instance name.
            zone: String, representing zone name, e.g. "us-central1-f"
            port: int, which COM port to read from, 1-4, default to 1.

        Returns:
            String, contents of the output.

        Raises:
            errors.DriverError: For malformed response.
        """
        return super(AndroidComputeClient, self).GetSerialPortOutput(instance,
            zone or self._zone, port)
    <mask token>
    <mask token>

<mask token>


class AndroidComputeClient(gcompute_client.ComputeClient):
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>

    def __init__(self, acloud_config, oauth2_credentials):
        """Initialize.

        Args:
            acloud_config: An AcloudConfig object.
            oauth2_credentials: An oauth2client.OAuth2Credentials instance.
        """
        super(AndroidComputeClient, self).__init__(acloud_config,
            oauth2_credentials)
        self._zone = acloud_config.zone
        self._machine_type = acloud_config.machine_type
        self._min_machine_size = acloud_config.min_machine_size
        self._network = acloud_config.network
        self._orientation = acloud_config.orientation
        self._resolution = acloud_config.resolution
        self._metadata = acloud_config.metadata_variable.copy()
        self._ssh_public_key_path = acloud_config.ssh_public_key_path
        self._launch_args = acloud_config.launch_args
        self._instance_name_pattern = acloud_config.instance_name_pattern
        self._AddPerInstanceSshkey()
        self._dict_report = {_ZONE: self._zone, _VERSION: config.GetVersion()}

    def _VerifyZoneByQuota(self):
        """Verify the zone must have enough quota to create instance.

        Returns:
            Boolean, True if zone have enough quota to create instance.

        Raises:
            errors.CheckGCEZonesQuotaError: the zone doesn't have enough quota.
        """
        if self.EnoughMetricsInZone(self._zone):
            return True
        raise errors.CheckGCEZonesQuotaError(
            'There is no enough quota in zone: %s' % self._zone)

    def _AddPerInstanceSshkey(self):
        """Add per-instance ssh key.

        Assign the ssh publick key to instacne then use ssh command to
        control remote instance via the ssh publick key. Added sshkey for two
        users. One is vsoc01, another is current user.

        """
        if self._ssh_public_key_path:
            rsa = self._LoadSshPublicKey(self._ssh_public_key_path)
            logger.info(
                'ssh_public_key_path is specified in config: %s, will add the key to the instance.'
                , self._ssh_public_key_path)
            self._metadata['sshKeys'] = '{0}:{2}\n{1}:{2}'.format(getpass.
                getuser(), constants.GCE_USER, rsa)
        else:
            logger.warning(
                'ssh_public_key_path is not specified in config, only project-wide key will be effective.'
                )

    @classmethod
    def _FormalizeName(cls, name):
        """Formalize the name to comply with RFC1035.

        The name must be 1-63 characters long and match the regular expression
        [a-z]([-a-z0-9]*[a-z0-9])? which means the first character must be a
        lowercase letter, and all following characters must be a dash,
        lowercase letter, or digit, except the last character, which cannot be
        a dash.

        Args:
          name: A string.

        Returns:
          name: A string that complies with RFC1035.
        """
        name = name.replace('_', '-').lower()
        name = name[:cls.NAME_LENGTH_LIMIT]
        if name[-1] == '-':
            name = name[:-1] + cls.REPLACER
        return name

    def _CheckMachineSize(self):
        """Check machine size.

        Check if the desired machine type |self._machine_type| meets
        the requirement of minimum machine size specified as
        |self._min_machine_size|.

        Raises:
            errors.DriverError: if check fails.
        """
        if self.CompareMachineSize(self._machine_type, self.
            _min_machine_size, self._zone) < 0:
            raise errors.DriverError(
                '%s does not meet the minimum required machine size %s' % (
                self._machine_type, self._min_machine_size))

    @classmethod
    def GenerateImageName(cls, build_target=None, build_id=None):
        """Generate an image name given build_target, build_id.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing image name.
        """
        if not build_target and not build_id:
            return 'image-' + uuid.uuid4().hex
        name = cls.IMAGE_NAME_FMT.format(build_target=build_target,
            build_id=build_id, uuid=uuid.uuid4().hex[:8])
        return cls._FormalizeName(name)

    @classmethod
    def GetDataDiskName(cls, instance):
        """Get data disk name for an instance.

        Args:
            instance: An instance_name.

        Returns:
            The corresponding data disk name.
        """
        name = cls.DATA_DISK_NAME_FMT.format(instance=instance)
        return cls._FormalizeName(name)

    def GenerateInstanceName(self, build_target=None, build_id=None):
        """Generate an instance name given build_target, build_id.

        Target is not used as instance name has a length limit.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing instance name.
        """
        name = self._instance_name_pattern.format(build_target=build_target,
            build_id=build_id, uuid=uuid.uuid4().hex[:8])
        return self._FormalizeName(name)

    def CreateDisk(self, disk_name, source_image, size_gb, zone=None,
        source_project=None, disk_type=gcompute_client.PersistentDiskType.
        STANDARD):
        """Create a gce disk.

        Args:
            disk_name: String, name of disk.
            source_image: String, name to the image name.
            size_gb: Integer, size in gigabytes.
            zone: String, name of the zone, e.g. us-central1-b.
            source_project: String, required if the image is located in a different
                            project.
            disk_type: String, a value from PersistentDiskType, STANDARD
                       for regular hard disk or SSD for solid state disk.
        """
        if self.CheckDiskExists(disk_name, self._zone):
            raise errors.DriverError(
                'Failed to create disk %s, already exists.' % disk_name)
        if source_image and not self.CheckImageExists(source_image):
            raise errors.DriverError(
                'Failed to create disk %s, source image %s does not exist.' %
                (disk_name, source_image))
        super(AndroidComputeClient, self).CreateDisk(disk_name,
            source_image=source_image, size_gb=size_gb, zone=zone or self._zone
            )

    @staticmethod
    def _LoadSshPublicKey(ssh_public_key_path):
        """Load the content of ssh public key from a file.

        Args:
            ssh_public_key_path: String, path to the public key file.
                               E.g. ~/.ssh/acloud_rsa.pub
        Returns:
            String, content of the file.

        Raises:
            errors.DriverError if the public key file does not exist
            or the content is not valid.
        """
        key_path = os.path.expanduser(ssh_public_key_path)
        if not os.path.exists(key_path):
            raise errors.DriverError(
                'SSH public key file %s does not exist.' % key_path)
        with open(key_path) as f:
            rsa = f.read()
            rsa = rsa.strip() if rsa else rsa
            utils.VerifyRsaPubKey(rsa)
        return rsa

    @utils.TimeExecute('Creating GCE Instance')
    def CreateInstance(self, instance, image_name, machine_type=None,
        metadata=None, network=None, zone=None, disk_args=None,
        image_project=None, gpu=None, extra_disk_name=None, avd_spec=None,
        extra_scopes=None, tags=None):
        """Create a gce instance with a gce image.

        Args:
            instance: String, instance name.
            image_name: String, source image used to create this disk.
            machine_type: String, representing machine_type,
                          e.g. "n1-standard-1"
            metadata: Dict, maps a metadata name to its value.
            network: String, representing network name, e.g. "default"
            zone: String, representing zone name, e.g. "us-central1-f"
            disk_args: A list of extra disk args (strings), see _GetDiskArgs
                       for example, if None, will create a disk using the given
                       image.
            image_project: String, name of the project where the image
                           belongs. Assume the default project if None.
            gpu: String, type of gpu to attach. e.g. "nvidia-tesla-k80", if
                 None no gpus will be attached. For more details see:
                 https://cloud.google.com/compute/docs/gpus/add-gpus
            extra_disk_name: String,the name of the extra disk to attach.
            avd_spec: AVDSpec object that tells us what we're going to create.
            extra_scopes: List, extra scopes (strings) to be passed to the
                          instance.
            tags: A list of tags to associate with the instance. e.g.
                 ["http-server", "https-server"]
        """
        self._CheckMachineSize()
        disk_args = self._GetDiskArgs(instance, image_name)
        metadata = self._metadata.copy()
        metadata['cfg_sta_display_resolution'] = self._resolution
        metadata['t_force_orientation'] = self._orientation
        metadata[constants.INS_KEY_AVD_TYPE] = avd_spec.avd_type
        metadata[constants.INS_KEY_DISPLAY] = '%sx%s (%s)' % (avd_spec.
            hw_property[constants.HW_X_RES], avd_spec.hw_property[constants
            .HW_Y_RES], avd_spec.hw_property[constants.HW_ALIAS_DPI])
        super(AndroidComputeClient, self).CreateInstance(instance,
            image_name, self._machine_type, metadata, self._network, self.
            _zone, disk_args, image_project, gpu, extra_disk_name,
            extra_scopes=extra_scopes, tags=tags)

    def CheckBootFailure(self, serial_out, instance):
        """Determine if serial output has indicated any boot failure.

        Subclass has to define this function to detect failures
        in the boot process

        Args:
            serial_out: string
            instance: string, instance name.

        Raises:
            Raises errors.DeviceBootError exception if a failure is detected.
        """
        pass

    def CheckBoot(self, instance):
        """Check once to see if boot completes.

        Args:
            instance: string, instance name.

        Returns:
            True if the BOOT_COMPLETED_MSG or BOOT_STARTED_MSG appears in serial
            port output, otherwise False.
        """
        try:
            serial_out = self.GetSerialPortOutput(instance=instance, port=1)
            self.CheckBootFailure(serial_out, instance)
            return (self.BOOT_COMPLETED_MSG in serial_out or self.
                BOOT_STARTED_MSG in serial_out)
        except errors.HttpError as e:
            if e.code == 400:
                logger.debug('CheckBoot: Instance is not ready yet %s', str(e))
                return False
            raise

    def WaitForBoot(self, instance, boot_timeout_secs=None):
        """Wait for boot to completes or hit timeout.

        Args:
            instance: string, instance name.
            boot_timeout_secs: Integer, the maximum time in seconds used to
                               wait for the AVD to boot.
        """
        boot_timeout_secs = (boot_timeout_secs or constants.
            DEFAULT_CF_BOOT_TIMEOUT)
        logger.info('Waiting for instance to boot up %s for %s secs',
            instance, boot_timeout_secs)
        timeout_exception = errors.DeviceBootTimeoutError(
            'Device %s did not finish on boot within timeout (%s secs)' % (
            instance, boot_timeout_secs))
        utils.PollAndWait(func=self.CheckBoot, expected_return=True,
            timeout_exception=timeout_exception, timeout_secs=
            boot_timeout_secs, sleep_interval_secs=self.
            BOOT_CHECK_INTERVAL_SECS, instance=instance)
        logger.info('Instance boot completed: %s', instance)

    def GetInstanceIP(self, instance, zone=None):
        """Get Instance IP given instance name.

        Args:
            instance: String, representing instance name.
            zone: String, representing zone name, e.g. "us-central1-f"

        Returns:
            ssh.IP object, that stores internal and external ip of the instance.
        """
        return super(AndroidComputeClient, self).GetInstanceIP(instance, 
            zone or self._zone)

    def GetSerialPortOutput(self, instance, zone=None, port=1):
        """Get serial port output.

        Args:
            instance: string, instance name.
            zone: String, representing zone name, e.g. "us-central1-f"
            port: int, which COM port to read from, 1-4, default to 1.

        Returns:
            String, contents of the output.

        Raises:
            errors.DriverError: For malformed response.
        """
        return super(AndroidComputeClient, self).GetSerialPortOutput(instance,
            zone or self._zone, port)

    def ExtendReportData(self, key, value):
        """Extend the report data.

        Args:
            key: string of key name.
            value: string of data value.
        """
        self._dict_report.update({key: value})

    @property
    def dict_report(self):
        """Return dict_report"""
        return self._dict_report

<mask token>
logger = logging.getLogger(__name__)
_ZONE = 'zone'
_VERSION = 'version'


class AndroidComputeClient(gcompute_client.ComputeClient):
    """Client that manages Anadroid Virtual Device."""
    IMAGE_NAME_FMT = 'img-{uuid}-{build_id}-{build_target}'
    DATA_DISK_NAME_FMT = 'data-{instance}'
    BOOT_COMPLETED_MSG = 'VIRTUAL_DEVICE_BOOT_COMPLETED'
    BOOT_STARTED_MSG = 'VIRTUAL_DEVICE_BOOT_STARTED'
    BOOT_CHECK_INTERVAL_SECS = 10
    OPERATION_TIMEOUT_SECS = 20 * 60
    NAME_LENGTH_LIMIT = 63
    REPLACER = 'e'

    def __init__(self, acloud_config, oauth2_credentials):
        """Initialize.

        Args:
            acloud_config: An AcloudConfig object.
            oauth2_credentials: An oauth2client.OAuth2Credentials instance.
        """
        super(AndroidComputeClient, self).__init__(acloud_config,
            oauth2_credentials)
        self._zone = acloud_config.zone
        self._machine_type = acloud_config.machine_type
        self._min_machine_size = acloud_config.min_machine_size
        self._network = acloud_config.network
        self._orientation = acloud_config.orientation
        self._resolution = acloud_config.resolution
        self._metadata = acloud_config.metadata_variable.copy()
        self._ssh_public_key_path = acloud_config.ssh_public_key_path
        self._launch_args = acloud_config.launch_args
        self._instance_name_pattern = acloud_config.instance_name_pattern
        self._AddPerInstanceSshkey()
        self._dict_report = {_ZONE: self._zone, _VERSION: config.GetVersion()}

    def _VerifyZoneByQuota(self):
        """Verify the zone must have enough quota to create instance.

        Returns:
            Boolean, True if zone have enough quota to create instance.

        Raises:
            errors.CheckGCEZonesQuotaError: the zone doesn't have enough quota.
        """
        if self.EnoughMetricsInZone(self._zone):
            return True
        raise errors.CheckGCEZonesQuotaError(
            'There is no enough quota in zone: %s' % self._zone)

    def _AddPerInstanceSshkey(self):
        """Add per-instance ssh key.

        Assign the ssh publick key to instacne then use ssh command to
        control remote instance via the ssh publick key. Added sshkey for two
        users. One is vsoc01, another is current user.

        """
        if self._ssh_public_key_path:
            rsa = self._LoadSshPublicKey(self._ssh_public_key_path)
            logger.info(
                'ssh_public_key_path is specified in config: %s, will add the key to the instance.'
                , self._ssh_public_key_path)
            self._metadata['sshKeys'] = '{0}:{2}\n{1}:{2}'.format(getpass.
                getuser(), constants.GCE_USER, rsa)
        else:
            logger.warning(
                'ssh_public_key_path is not specified in config, only project-wide key will be effective.'
                )

    @classmethod
    def _FormalizeName(cls, name):
        """Formalize the name to comply with RFC1035.

        The name must be 1-63 characters long and match the regular expression
        [a-z]([-a-z0-9]*[a-z0-9])? which means the first character must be a
        lowercase letter, and all following characters must be a dash,
        lowercase letter, or digit, except the last character, which cannot be
        a dash.

        Args:
          name: A string.

        Returns:
          name: A string that complies with RFC1035.
        """
        name = name.replace('_', '-').lower()
        name = name[:cls.NAME_LENGTH_LIMIT]
        if name[-1] == '-':
            name = name[:-1] + cls.REPLACER
        return name

    def _CheckMachineSize(self):
        """Check machine size.

        Check if the desired machine type |self._machine_type| meets
        the requirement of minimum machine size specified as
        |self._min_machine_size|.

        Raises:
            errors.DriverError: if check fails.
        """
        if self.CompareMachineSize(self._machine_type, self.
            _min_machine_size, self._zone) < 0:
            raise errors.DriverError(
                '%s does not meet the minimum required machine size %s' % (
                self._machine_type, self._min_machine_size))

    @classmethod
    def GenerateImageName(cls, build_target=None, build_id=None):
        """Generate an image name given build_target, build_id.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing image name.
        """
        if not build_target and not build_id:
            return 'image-' + uuid.uuid4().hex
        name = cls.IMAGE_NAME_FMT.format(build_target=build_target,
            build_id=build_id, uuid=uuid.uuid4().hex[:8])
        return cls._FormalizeName(name)

    @classmethod
    def GetDataDiskName(cls, instance):
        """Get data disk name for an instance.

        Args:
            instance: An instance_name.

        Returns:
            The corresponding data disk name.
        """
        name = cls.DATA_DISK_NAME_FMT.format(instance=instance)
        return cls._FormalizeName(name)

    def GenerateInstanceName(self, build_target=None, build_id=None):
        """Generate an instance name given build_target, build_id.

        Target is not used as instance name has a length limit.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing instance name.
        """
        name = self._instance_name_pattern.format(build_target=build_target,
            build_id=build_id, uuid=uuid.uuid4().hex[:8])
        return self._FormalizeName(name)

    def CreateDisk(self, disk_name, source_image, size_gb, zone=None,
        source_project=None, disk_type=gcompute_client.PersistentDiskType.
        STANDARD):
        """Create a gce disk.

        Args:
            disk_name: String, name of disk.
            source_image: String, name to the image name.
            size_gb: Integer, size in gigabytes.
            zone: String, name of the zone, e.g. us-central1-b.
            source_project: String, required if the image is located in a different
                            project.
            disk_type: String, a value from PersistentDiskType, STANDARD
                       for regular hard disk or SSD for solid state disk.
        """
        if self.CheckDiskExists(disk_name, self._zone):
            raise errors.DriverError(
                'Failed to create disk %s, already exists.' % disk_name)
        if source_image and not self.CheckImageExists(source_image):
            raise errors.DriverError(
                'Failed to create disk %s, source image %s does not exist.' %
                (disk_name, source_image))
        super(AndroidComputeClient, self).CreateDisk(disk_name,
            source_image=source_image, size_gb=size_gb, zone=zone or self._zone
            )

    @staticmethod
    def _LoadSshPublicKey(ssh_public_key_path):
        """Load the content of ssh public key from a file.

        Args:
            ssh_public_key_path: String, path to the public key file.
                               E.g. ~/.ssh/acloud_rsa.pub
        Returns:
            String, content of the file.

        Raises:
            errors.DriverError if the public key file does not exist
            or the content is not valid.
        """
        key_path = os.path.expanduser(ssh_public_key_path)
        if not os.path.exists(key_path):
            raise errors.DriverError(
                'SSH public key file %s does not exist.' % key_path)
        with open(key_path) as f:
            rsa = f.read()
            rsa = rsa.strip() if rsa else rsa
            utils.VerifyRsaPubKey(rsa)
        return rsa

    @utils.TimeExecute('Creating GCE Instance')
    def CreateInstance(self, instance, image_name, machine_type=None,
        metadata=None, network=None, zone=None, disk_args=None,
        image_project=None, gpu=None, extra_disk_name=None, avd_spec=None,
        extra_scopes=None, tags=None):
        """Create a gce instance with a gce image.

        Args:
            instance: String, instance name.
            image_name: String, source image used to create this disk.
            machine_type: String, representing machine_type,
                          e.g. "n1-standard-1"
            metadata: Dict, maps a metadata name to its value.
            network: String, representing network name, e.g. "default"
            zone: String, representing zone name, e.g. "us-central1-f"
            disk_args: A list of extra disk args (strings), see _GetDiskArgs
                       for example, if None, will create a disk using the given
                       image.
            image_project: String, name of the project where the image
                           belongs. Assume the default project if None.
            gpu: String, type of gpu to attach. e.g. "nvidia-tesla-k80", if
                 None no gpus will be attached. For more details see:
                 https://cloud.google.com/compute/docs/gpus/add-gpus
            extra_disk_name: String,the name of the extra disk to attach.
            avd_spec: AVDSpec object that tells us what we're going to create.
            extra_scopes: List, extra scopes (strings) to be passed to the
                          instance.
            tags: A list of tags to associate with the instance. e.g.
                 ["http-server", "https-server"]
        """
        self._CheckMachineSize()
        disk_args = self._GetDiskArgs(instance, image_name)
        metadata = self._metadata.copy()
        metadata['cfg_sta_display_resolution'] = self._resolution
        metadata['t_force_orientation'] = self._orientation
        metadata[constants.INS_KEY_AVD_TYPE] = avd_spec.avd_type
        metadata[constants.INS_KEY_DISPLAY] = '%sx%s (%s)' % (avd_spec.
            hw_property[constants.HW_X_RES], avd_spec.hw_property[constants
            .HW_Y_RES], avd_spec.hw_property[constants.HW_ALIAS_DPI])
        super(AndroidComputeClient, self).CreateInstance(instance,
            image_name, self._machine_type, metadata, self._network, self.
            _zone, disk_args, image_project, gpu, extra_disk_name,
            extra_scopes=extra_scopes, tags=tags)

    def CheckBootFailure(self, serial_out, instance):
        """Determine if serial output has indicated any boot failure.

        Subclass has to define this function to detect failures
        in the boot process

        Args:
            serial_out: string
            instance: string, instance name.

        Raises:
            Raises errors.DeviceBootError exception if a failure is detected.
        """
        pass

    def CheckBoot(self, instance):
        """Check once to see if boot completes.

        Args:
            instance: string, instance name.

        Returns:
            True if the BOOT_COMPLETED_MSG or BOOT_STARTED_MSG appears in serial
            port output, otherwise False.
        """
        try:
            serial_out = self.GetSerialPortOutput(instance=instance, port=1)
            self.CheckBootFailure(serial_out, instance)
            return (self.BOOT_COMPLETED_MSG in serial_out or self.
                BOOT_STARTED_MSG in serial_out)
        except errors.HttpError as e:
            if e.code == 400:
                logger.debug('CheckBoot: Instance is not ready yet %s', str(e))
                return False
            raise

    def WaitForBoot(self, instance, boot_timeout_secs=None):
        """Wait for boot to completes or hit timeout.

        Args:
            instance: string, instance name.
            boot_timeout_secs: Integer, the maximum time in seconds used to
                               wait for the AVD to boot.
        """
        boot_timeout_secs = (boot_timeout_secs or constants.
            DEFAULT_CF_BOOT_TIMEOUT)
        logger.info('Waiting for instance to boot up %s for %s secs',
            instance, boot_timeout_secs)
        timeout_exception = errors.DeviceBootTimeoutError(
            'Device %s did not finish on boot within timeout (%s secs)' % (
            instance, boot_timeout_secs))
        utils.PollAndWait(func=self.CheckBoot, expected_return=True,
            timeout_exception=timeout_exception, timeout_secs=
            boot_timeout_secs, sleep_interval_secs=self.
            BOOT_CHECK_INTERVAL_SECS, instance=instance)
        logger.info('Instance boot completed: %s', instance)

    def GetInstanceIP(self, instance, zone=None):
        """Get Instance IP given instance name.

        Args:
            instance: String, representing instance name.
            zone: String, representing zone name, e.g. "us-central1-f"

        Returns:
            ssh.IP object, that stores internal and external ip of the instance.
        """
        return super(AndroidComputeClient, self).GetInstanceIP(instance, 
            zone or self._zone)

    def GetSerialPortOutput(self, instance, zone=None, port=1):
        """Get serial port output.

        Args:
            instance: string, instance name.
            zone: String, representing zone name, e.g. "us-central1-f"
            port: int, which COM port to read from, 1-4, default to 1.

        Returns:
            String, contents of the output.

        Raises:
            errors.DriverError: For malformed response.
        """
        return super(AndroidComputeClient, self).GetSerialPortOutput(instance,
            zone or self._zone, port)

    def ExtendReportData(self, key, value):
        """Extend the report data.

        Args:
            key: string of key name.
            value: string of data value.
        """
        self._dict_report.update({key: value})

    @property
    def dict_report(self):
        """Return dict_report"""
        return self._dict_report

<mask token>
import getpass
import logging
import os
import uuid
from acloud import errors
from acloud.internal import constants
from acloud.internal.lib import gcompute_client
from acloud.internal.lib import utils
from acloud.public import config
logger = logging.getLogger(__name__)
_ZONE = 'zone'
_VERSION = 'version'


class AndroidComputeClient(gcompute_client.ComputeClient):
    """Client that manages Anadroid Virtual Device."""
    IMAGE_NAME_FMT = 'img-{uuid}-{build_id}-{build_target}'
    DATA_DISK_NAME_FMT = 'data-{instance}'
    BOOT_COMPLETED_MSG = 'VIRTUAL_DEVICE_BOOT_COMPLETED'
    BOOT_STARTED_MSG = 'VIRTUAL_DEVICE_BOOT_STARTED'
    BOOT_CHECK_INTERVAL_SECS = 10
    OPERATION_TIMEOUT_SECS = 20 * 60
    NAME_LENGTH_LIMIT = 63
    REPLACER = 'e'

    def __init__(self, acloud_config, oauth2_credentials):
        """Initialize.

        Args:
            acloud_config: An AcloudConfig object.
            oauth2_credentials: An oauth2client.OAuth2Credentials instance.
        """
        super(AndroidComputeClient, self).__init__(acloud_config,
            oauth2_credentials)
        self._zone = acloud_config.zone
        self._machine_type = acloud_config.machine_type
        self._min_machine_size = acloud_config.min_machine_size
        self._network = acloud_config.network
        self._orientation = acloud_config.orientation
        self._resolution = acloud_config.resolution
        self._metadata = acloud_config.metadata_variable.copy()
        self._ssh_public_key_path = acloud_config.ssh_public_key_path
        self._launch_args = acloud_config.launch_args
        self._instance_name_pattern = acloud_config.instance_name_pattern
        self._AddPerInstanceSshkey()
        self._dict_report = {_ZONE: self._zone, _VERSION: config.GetVersion()}

    def _VerifyZoneByQuota(self):
        """Verify the zone must have enough quota to create instance.

        Returns:
            Boolean, True if zone have enough quota to create instance.

        Raises:
            errors.CheckGCEZonesQuotaError: the zone doesn't have enough quota.
        """
        if self.EnoughMetricsInZone(self._zone):
            return True
        raise errors.CheckGCEZonesQuotaError(
            'There is no enough quota in zone: %s' % self._zone)

    def _AddPerInstanceSshkey(self):
        """Add per-instance ssh key.

        Assign the ssh publick key to instacne then use ssh command to
        control remote instance via the ssh publick key. Added sshkey for two
        users. One is vsoc01, another is current user.

        """
        if self._ssh_public_key_path:
            rsa = self._LoadSshPublicKey(self._ssh_public_key_path)
            logger.info(
                'ssh_public_key_path is specified in config: %s, will add the key to the instance.'
                , self._ssh_public_key_path)
            self._metadata['sshKeys'] = '{0}:{2}\n{1}:{2}'.format(getpass.
                getuser(), constants.GCE_USER, rsa)
        else:
            logger.warning(
                'ssh_public_key_path is not specified in config, only project-wide key will be effective.'
                )

    @classmethod
    def _FormalizeName(cls, name):
        """Formalize the name to comply with RFC1035.

        The name must be 1-63 characters long and match the regular expression
        [a-z]([-a-z0-9]*[a-z0-9])? which means the first character must be a
        lowercase letter, and all following characters must be a dash,
        lowercase letter, or digit, except the last character, which cannot be
        a dash.

        Args:
          name: A string.

        Returns:
          name: A string that complies with RFC1035.
        """
        name = name.replace('_', '-').lower()
        name = name[:cls.NAME_LENGTH_LIMIT]
        if name[-1] == '-':
            name = name[:-1] + cls.REPLACER
        return name

    def _CheckMachineSize(self):
        """Check machine size.

        Check if the desired machine type |self._machine_type| meets
        the requirement of minimum machine size specified as
        |self._min_machine_size|.

        Raises:
            errors.DriverError: if check fails.
        """
        if self.CompareMachineSize(self._machine_type, self.
            _min_machine_size, self._zone) < 0:
            raise errors.DriverError(
                '%s does not meet the minimum required machine size %s' % (
                self._machine_type, self._min_machine_size))

    @classmethod
    def GenerateImageName(cls, build_target=None, build_id=None):
        """Generate an image name given build_target, build_id.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing image name.
        """
        if not build_target and not build_id:
            return 'image-' + uuid.uuid4().hex
        name = cls.IMAGE_NAME_FMT.format(build_target=build_target,
            build_id=build_id, uuid=uuid.uuid4().hex[:8])
        return cls._FormalizeName(name)

    @classmethod
    def GetDataDiskName(cls, instance):
        """Get data disk name for an instance.

        Args:
            instance: An instance_name.

        Returns:
            The corresponding data disk name.
        """
        name = cls.DATA_DISK_NAME_FMT.format(instance=instance)
        return cls._FormalizeName(name)

    def GenerateInstanceName(self, build_target=None, build_id=None):
        """Generate an instance name given build_target, build_id.

        Target is not used as instance name has a length limit.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing instance name.
        """
        name = self._instance_name_pattern.format(build_target=build_target,
            build_id=build_id, uuid=uuid.uuid4().hex[:8])
        return self._FormalizeName(name)

    def CreateDisk(self, disk_name, source_image, size_gb, zone=None,
        source_project=None, disk_type=gcompute_client.PersistentDiskType.
        STANDARD):
        """Create a gce disk.

        Args:
            disk_name: String, name of disk.
            source_image: String, name to the image name.
            size_gb: Integer, size in gigabytes.
            zone: String, name of the zone, e.g. us-central1-b.
            source_project: String, required if the image is located in a different
                            project.
            disk_type: String, a value from PersistentDiskType, STANDARD
                       for regular hard disk or SSD for solid state disk.
        """
        if self.CheckDiskExists(disk_name, self._zone):
            raise errors.DriverError(
                'Failed to create disk %s, already exists.' % disk_name)
        if source_image and not self.CheckImageExists(source_image):
            raise errors.DriverError(
                'Failed to create disk %s, source image %s does not exist.' %
                (disk_name, source_image))
        super(AndroidComputeClient, self).CreateDisk(disk_name,
            source_image=source_image, size_gb=size_gb, zone=zone or self._zone
            )

    @staticmethod
    def _LoadSshPublicKey(ssh_public_key_path):
        """Load the content of ssh public key from a file.

        Args:
            ssh_public_key_path: String, path to the public key file.
                               E.g. ~/.ssh/acloud_rsa.pub
        Returns:
            String, content of the file.

        Raises:
            errors.DriverError if the public key file does not exist
            or the content is not valid.
        """
        key_path = os.path.expanduser(ssh_public_key_path)
        if not os.path.exists(key_path):
            raise errors.DriverError(
                'SSH public key file %s does not exist.' % key_path)
        with open(key_path) as f:
            rsa = f.read()
            rsa = rsa.strip() if rsa else rsa
            utils.VerifyRsaPubKey(rsa)
        return rsa

    @utils.TimeExecute('Creating GCE Instance')
    def CreateInstance(self, instance, image_name, machine_type=None,
        metadata=None, network=None, zone=None, disk_args=None,
        image_project=None, gpu=None, extra_disk_name=None, avd_spec=None,
        extra_scopes=None, tags=None):
        """Create a gce instance with a gce image.

        Args:
            instance: String, instance name.
            image_name: String, source image used to create this disk.
            machine_type: String, representing machine_type,
                          e.g. "n1-standard-1"
            metadata: Dict, maps a metadata name to its value.
            network: String, representing network name, e.g. "default"
            zone: String, representing zone name, e.g. "us-central1-f"
            disk_args: A list of extra disk args (strings), see _GetDiskArgs
                       for example, if None, will create a disk using the given
                       image.
            image_project: String, name of the project where the image
                           belongs. Assume the default project if None.
            gpu: String, type of gpu to attach. e.g. "nvidia-tesla-k80", if
                 None no gpus will be attached. For more details see:
                 https://cloud.google.com/compute/docs/gpus/add-gpus
            extra_disk_name: String,the name of the extra disk to attach.
            avd_spec: AVDSpec object that tells us what we're going to create.
            extra_scopes: List, extra scopes (strings) to be passed to the
                          instance.
            tags: A list of tags to associate with the instance. e.g.
                 ["http-server", "https-server"]
        """
        self._CheckMachineSize()
        disk_args = self._GetDiskArgs(instance, image_name)
        metadata = self._metadata.copy()
        metadata['cfg_sta_display_resolution'] = self._resolution
        metadata['t_force_orientation'] = self._orientation
        metadata[constants.INS_KEY_AVD_TYPE] = avd_spec.avd_type
        metadata[constants.INS_KEY_DISPLAY] = '%sx%s (%s)' % (avd_spec.
            hw_property[constants.HW_X_RES], avd_spec.hw_property[constants
            .HW_Y_RES], avd_spec.hw_property[constants.HW_ALIAS_DPI])
        super(AndroidComputeClient, self).CreateInstance(instance,
            image_name, self._machine_type, metadata, self._network, self.
            _zone, disk_args, image_project, gpu, extra_disk_name,
            extra_scopes=extra_scopes, tags=tags)

    def CheckBootFailure(self, serial_out, instance):
        """Determine if serial output has indicated any boot failure.

        Subclass has to define this function to detect failures
        in the boot process

        Args:
            serial_out: string
            instance: string, instance name.

        Raises:
            Raises errors.DeviceBootError exception if a failure is detected.
        """
        pass

    def CheckBoot(self, instance):
        """Check once to see if boot completes.

        Args:
            instance: string, instance name.

        Returns:
            True if the BOOT_COMPLETED_MSG or BOOT_STARTED_MSG appears in serial
            port output, otherwise False.
        """
        try:
            serial_out = self.GetSerialPortOutput(instance=instance, port=1)
            self.CheckBootFailure(serial_out, instance)
            return (self.BOOT_COMPLETED_MSG in serial_out or self.
                BOOT_STARTED_MSG in serial_out)
        except errors.HttpError as e:
            if e.code == 400:
                logger.debug('CheckBoot: Instance is not ready yet %s', str(e))
                return False
            raise

    def WaitForBoot(self, instance, boot_timeout_secs=None):
        """Wait for boot to completes or hit timeout.

        Args:
            instance: string, instance name.
            boot_timeout_secs: Integer, the maximum time in seconds used to
                               wait for the AVD to boot.
        """
        boot_timeout_secs = (boot_timeout_secs or constants.
            DEFAULT_CF_BOOT_TIMEOUT)
        logger.info('Waiting for instance to boot up %s for %s secs',
            instance, boot_timeout_secs)
        timeout_exception = errors.DeviceBootTimeoutError(
            'Device %s did not finish on boot within timeout (%s secs)' % (
            instance, boot_timeout_secs))
        utils.PollAndWait(func=self.CheckBoot, expected_return=True,
            timeout_exception=timeout_exception, timeout_secs=
            boot_timeout_secs, sleep_interval_secs=self.
            BOOT_CHECK_INTERVAL_SECS, instance=instance)
        logger.info('Instance boot completed: %s', instance)

    def GetInstanceIP(self, instance, zone=None):
        """Get Instance IP given instance name.

        Args:
            instance: String, representing instance name.
            zone: String, representing zone name, e.g. "us-central1-f"

        Returns:
            ssh.IP object, that stores internal and external ip of the instance.
        """
        return super(AndroidComputeClient, self).GetInstanceIP(instance, 
            zone or self._zone)

    def GetSerialPortOutput(self, instance, zone=None, port=1):
        """Get serial port output.

        Args:
            instance: string, instance name.
            zone: String, representing zone name, e.g. "us-central1-f"
            port: int, which COM port to read from, 1-4, default to 1.

        Returns:
            String, contents of the output.

        Raises:
            errors.DriverError: For malformed response.
        """
        return super(AndroidComputeClient, self).GetSerialPortOutput(instance,
            zone or self._zone, port)

    def ExtendReportData(self, key, value):
        """Extend the report data.

        Args:
            key: string of key name.
            value: string of data value.
        """
        self._dict_report.update({key: value})

    @property
    def dict_report(self):
        """Return dict_report"""
        return self._dict_report

#!/usr/bin/env python
#
# Copyright 2016 - 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.
"""A client that manages Android compute engine instances.

** AndroidComputeClient **

AndroidComputeClient derives from ComputeClient. It manges a google
compute engine project that is setup for running Android instances.
It knows how to create android GCE images and instances.

** Class hierarchy **

  base_cloud_client.BaseCloudApiClient
                ^
                |
       gcompute_client.ComputeClient
                ^
                |
    gcompute_client.AndroidComputeClient
"""

import getpass
import logging
import os
import uuid

from acloud import errors
from acloud.internal import constants
from acloud.internal.lib import gcompute_client
from acloud.internal.lib import utils
from acloud.public import config


logger = logging.getLogger(__name__)
_ZONE = "zone"
_VERSION = "version"


class AndroidComputeClient(gcompute_client.ComputeClient):
    """Client that manages Anadroid Virtual Device."""
    IMAGE_NAME_FMT = "img-{uuid}-{build_id}-{build_target}"
    DATA_DISK_NAME_FMT = "data-{instance}"
    BOOT_COMPLETED_MSG = "VIRTUAL_DEVICE_BOOT_COMPLETED"
    BOOT_STARTED_MSG = "VIRTUAL_DEVICE_BOOT_STARTED"
    BOOT_CHECK_INTERVAL_SECS = 10
    OPERATION_TIMEOUT_SECS = 20 * 60  # Override parent value, 20 mins

    NAME_LENGTH_LIMIT = 63
    # If the generated name ends with '-', replace it with REPLACER.
    REPLACER = "e"

    def __init__(self, acloud_config, oauth2_credentials):
        """Initialize.

        Args:
            acloud_config: An AcloudConfig object.
            oauth2_credentials: An oauth2client.OAuth2Credentials instance.
        """
        super(AndroidComputeClient, self).__init__(acloud_config,
                                                   oauth2_credentials)
        self._zone = acloud_config.zone
        self._machine_type = acloud_config.machine_type
        self._min_machine_size = acloud_config.min_machine_size
        self._network = acloud_config.network
        self._orientation = acloud_config.orientation
        self._resolution = acloud_config.resolution
        self._metadata = acloud_config.metadata_variable.copy()
        self._ssh_public_key_path = acloud_config.ssh_public_key_path
        self._launch_args = acloud_config.launch_args
        self._instance_name_pattern = acloud_config.instance_name_pattern
        self._AddPerInstanceSshkey()
        self._dict_report = {_ZONE: self._zone,
                             _VERSION: config.GetVersion()}

    # TODO(147047953): New args to contorl zone metrics check.
    def _VerifyZoneByQuota(self):
        """Verify the zone must have enough quota to create instance.

        Returns:
            Boolean, True if zone have enough quota to create instance.

        Raises:
            errors.CheckGCEZonesQuotaError: the zone doesn't have enough quota.
        """
        if self.EnoughMetricsInZone(self._zone):
            return True
        raise errors.CheckGCEZonesQuotaError(
            "There is no enough quota in zone: %s" % self._zone)

    def _AddPerInstanceSshkey(self):
        """Add per-instance ssh key.

        Assign the ssh publick key to instacne then use ssh command to
        control remote instance via the ssh publick key. Added sshkey for two
        users. One is vsoc01, another is current user.

        """
        if self._ssh_public_key_path:
            rsa = self._LoadSshPublicKey(self._ssh_public_key_path)
            logger.info("ssh_public_key_path is specified in config: %s, "
                        "will add the key to the instance.",
                        self._ssh_public_key_path)
            self._metadata["sshKeys"] = "{0}:{2}\n{1}:{2}".format(getpass.getuser(),
                                                                  constants.GCE_USER,
                                                                  rsa)
        else:
            logger.warning(
                "ssh_public_key_path is not specified in config, "
                "only project-wide key will be effective.")

    @classmethod
    def _FormalizeName(cls, name):
        """Formalize the name to comply with RFC1035.

        The name must be 1-63 characters long and match the regular expression
        [a-z]([-a-z0-9]*[a-z0-9])? which means the first character must be a
        lowercase letter, and all following characters must be a dash,
        lowercase letter, or digit, except the last character, which cannot be
        a dash.

        Args:
          name: A string.

        Returns:
          name: A string that complies with RFC1035.
        """
        name = name.replace("_", "-").lower()
        name = name[:cls.NAME_LENGTH_LIMIT]
        if name[-1] == "-":
            name = name[:-1] + cls.REPLACER
        return name

    def _CheckMachineSize(self):
        """Check machine size.

        Check if the desired machine type |self._machine_type| meets
        the requirement of minimum machine size specified as
        |self._min_machine_size|.

        Raises:
            errors.DriverError: if check fails.
        """
        if self.CompareMachineSize(self._machine_type, self._min_machine_size,
                                   self._zone) < 0:
            raise errors.DriverError(
                "%s does not meet the minimum required machine size %s" %
                (self._machine_type, self._min_machine_size))

    @classmethod
    def GenerateImageName(cls, build_target=None, build_id=None):
        """Generate an image name given build_target, build_id.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing image name.
        """
        if not build_target and not build_id:
            return "image-" + uuid.uuid4().hex
        name = cls.IMAGE_NAME_FMT.format(
            build_target=build_target,
            build_id=build_id,
            uuid=uuid.uuid4().hex[:8])
        return cls._FormalizeName(name)

    @classmethod
    def GetDataDiskName(cls, instance):
        """Get data disk name for an instance.

        Args:
            instance: An instance_name.

        Returns:
            The corresponding data disk name.
        """
        name = cls.DATA_DISK_NAME_FMT.format(instance=instance)
        return cls._FormalizeName(name)

    def GenerateInstanceName(self, build_target=None, build_id=None):
        """Generate an instance name given build_target, build_id.

        Target is not used as instance name has a length limit.

        Args:
            build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
            build_id: Build id, a string, e.g. "2263051", "P2804227"

        Returns:
            A string, representing instance name.
        """
        name = self._instance_name_pattern.format(build_target=build_target,
                                                  build_id=build_id,
                                                  uuid=uuid.uuid4().hex[:8])
        return self._FormalizeName(name)

    def CreateDisk(self,
                   disk_name,
                   source_image,
                   size_gb,
                   zone=None,
                   source_project=None,
                   disk_type=gcompute_client.PersistentDiskType.STANDARD):
        """Create a gce disk.

        Args:
            disk_name: String, name of disk.
            source_image: String, name to the image name.
            size_gb: Integer, size in gigabytes.
            zone: String, name of the zone, e.g. us-central1-b.
            source_project: String, required if the image is located in a different
                            project.
            disk_type: String, a value from PersistentDiskType, STANDARD
                       for regular hard disk or SSD for solid state disk.
        """
        if self.CheckDiskExists(disk_name, self._zone):
            raise errors.DriverError(
                "Failed to create disk %s, already exists." % disk_name)
        if source_image and not self.CheckImageExists(source_image):
            raise errors.DriverError(
                "Failed to create disk %s, source image %s does not exist." %
                (disk_name, source_image))
        super(AndroidComputeClient, self).CreateDisk(
            disk_name,
            source_image=source_image,
            size_gb=size_gb,
            zone=zone or self._zone)

    @staticmethod
    def _LoadSshPublicKey(ssh_public_key_path):
        """Load the content of ssh public key from a file.

        Args:
            ssh_public_key_path: String, path to the public key file.
                               E.g. ~/.ssh/acloud_rsa.pub
        Returns:
            String, content of the file.

        Raises:
            errors.DriverError if the public key file does not exist
            or the content is not valid.
        """
        key_path = os.path.expanduser(ssh_public_key_path)
        if not os.path.exists(key_path):
            raise errors.DriverError(
                "SSH public key file %s does not exist." % key_path)

        with open(key_path) as f:
            rsa = f.read()
            rsa = rsa.strip() if rsa else rsa
            utils.VerifyRsaPubKey(rsa)
        return rsa

    # pylint: disable=too-many-locals, arguments-differ
    @utils.TimeExecute("Creating GCE Instance")
    def CreateInstance(self,
                       instance,
                       image_name,
                       machine_type=None,
                       metadata=None,
                       network=None,
                       zone=None,
                       disk_args=None,
                       image_project=None,
                       gpu=None,
                       extra_disk_name=None,
                       avd_spec=None,
                       extra_scopes=None,
                       tags=None):
        """Create a gce instance with a gce image.

        Args:
            instance: String, instance name.
            image_name: String, source image used to create this disk.
            machine_type: String, representing machine_type,
                          e.g. "n1-standard-1"
            metadata: Dict, maps a metadata name to its value.
            network: String, representing network name, e.g. "default"
            zone: String, representing zone name, e.g. "us-central1-f"
            disk_args: A list of extra disk args (strings), see _GetDiskArgs
                       for example, if None, will create a disk using the given
                       image.
            image_project: String, name of the project where the image
                           belongs. Assume the default project if None.
            gpu: String, type of gpu to attach. e.g. "nvidia-tesla-k80", if
                 None no gpus will be attached. For more details see:
                 https://cloud.google.com/compute/docs/gpus/add-gpus
            extra_disk_name: String,the name of the extra disk to attach.
            avd_spec: AVDSpec object that tells us what we're going to create.
            extra_scopes: List, extra scopes (strings) to be passed to the
                          instance.
            tags: A list of tags to associate with the instance. e.g.
                 ["http-server", "https-server"]
        """
        self._CheckMachineSize()
        disk_args = self._GetDiskArgs(instance, image_name)
        metadata = self._metadata.copy()
        metadata["cfg_sta_display_resolution"] = self._resolution
        metadata["t_force_orientation"] = self._orientation
        metadata[constants.INS_KEY_AVD_TYPE] = avd_spec.avd_type

        # Use another METADATA_DISPLAY to record resolution which will be
        # retrieved in acloud list cmd. We try not to use cvd_01_x_res
        # since cvd_01_xxx metadata is going to deprecated by cuttlefish.
        metadata[constants.INS_KEY_DISPLAY] = ("%sx%s (%s)" % (
            avd_spec.hw_property[constants.HW_X_RES],
            avd_spec.hw_property[constants.HW_Y_RES],
            avd_spec.hw_property[constants.HW_ALIAS_DPI]))

        super(AndroidComputeClient, self).CreateInstance(
            instance, image_name, self._machine_type, metadata, self._network,
            self._zone, disk_args, image_project, gpu, extra_disk_name,
            extra_scopes=extra_scopes, tags=tags)

    def CheckBootFailure(self, serial_out, instance):
        """Determine if serial output has indicated any boot failure.

        Subclass has to define this function to detect failures
        in the boot process

        Args:
            serial_out: string
            instance: string, instance name.

        Raises:
            Raises errors.DeviceBootError exception if a failure is detected.
        """
        pass

    def CheckBoot(self, instance):
        """Check once to see if boot completes.

        Args:
            instance: string, instance name.

        Returns:
            True if the BOOT_COMPLETED_MSG or BOOT_STARTED_MSG appears in serial
            port output, otherwise False.
        """
        try:
            serial_out = self.GetSerialPortOutput(instance=instance, port=1)
            self.CheckBootFailure(serial_out, instance)
            return ((self.BOOT_COMPLETED_MSG in serial_out)
                    or (self.BOOT_STARTED_MSG in serial_out))
        except errors.HttpError as e:
            if e.code == 400:
                logger.debug("CheckBoot: Instance is not ready yet %s", str(e))
                return False
            raise

    def WaitForBoot(self, instance, boot_timeout_secs=None):
        """Wait for boot to completes or hit timeout.

        Args:
            instance: string, instance name.
            boot_timeout_secs: Integer, the maximum time in seconds used to
                               wait for the AVD to boot.
        """
        boot_timeout_secs = boot_timeout_secs or constants.DEFAULT_CF_BOOT_TIMEOUT
        logger.info("Waiting for instance to boot up %s for %s secs",
                    instance, boot_timeout_secs)
        timeout_exception = errors.DeviceBootTimeoutError(
            "Device %s did not finish on boot within timeout (%s secs)" %
            (instance, boot_timeout_secs))
        utils.PollAndWait(
            func=self.CheckBoot,
            expected_return=True,
            timeout_exception=timeout_exception,
            timeout_secs=boot_timeout_secs,
            sleep_interval_secs=self.BOOT_CHECK_INTERVAL_SECS,
            instance=instance)
        logger.info("Instance boot completed: %s", instance)

    def GetInstanceIP(self, instance, zone=None):
        """Get Instance IP given instance name.

        Args:
            instance: String, representing instance name.
            zone: String, representing zone name, e.g. "us-central1-f"

        Returns:
            ssh.IP object, that stores internal and external ip of the instance.
        """
        return super(AndroidComputeClient, self).GetInstanceIP(
            instance, zone or self._zone)

    def GetSerialPortOutput(self, instance, zone=None, port=1):
        """Get serial port output.

        Args:
            instance: string, instance name.
            zone: String, representing zone name, e.g. "us-central1-f"
            port: int, which COM port to read from, 1-4, default to 1.

        Returns:
            String, contents of the output.

        Raises:
            errors.DriverError: For malformed response.
        """
        return super(AndroidComputeClient, self).GetSerialPortOutput(
            instance, zone or self._zone, port)

    def ExtendReportData(self, key, value):
        """Extend the report data.

        Args:
            key: string of key name.
            value: string of data value.
        """
        self._dict_report.update({key: value})

    @property
    def dict_report(self):
        """Return dict_report"""
        return self._dict_report
