# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
"""
ElasticHosts Driver
"""
import re
import time
import base64
import httplib

try:
    import json
except:
    import simplejson as json

from libcloud.common.base import ConnectionUserAndKey, Response
from libcloud.common.types import InvalidCredsError, MalformedResponseError
from libcloud.compute.types import Provider, NodeState
from libcloud.compute.base import NodeDriver, NodeSize, Node
from libcloud.compute.base import NodeImage
from libcloud.compute.deployment import ScriptDeployment, SSHKeyDeployment, MultiStepDeployment

# API end-points
API_ENDPOINTS = {
    'uk-1': {
        'name': 'London Peer 1',
        'country': 'United Kingdom',
        'host': 'api.lon-p.elastichosts.com'
    },
     'uk-2': {
        'name': 'London BlueSquare',
        'country': 'United Kingdom',
        'host': 'api.lon-b.elastichosts.com'
    },
     'us-1': {
        'name': 'San Antonio Peer 1',
        'country': 'United States',
        'host': 'api.sat-p.elastichosts.com'
    },
}

# Default API end-point for the base connection clase.
DEFAULT_ENDPOINT = 'us-1'

# ElasticHosts doesn't specify special instance types, so I just specified
# some plans based on the pricing page
# (http://www.elastichosts.com/cloud-hosting/pricing)
# and other provides.
#
# Basically for CPU any value between 500Mhz and 20000Mhz should work,
# 256MB to 8192MB for ram and 1GB to 2TB for disk.
INSTANCE_TYPES = {
    'small': {
        'id': 'small',
        'name': 'Small instance',
        'cpu': 2000,
        'memory': 1700,
        'disk': 160,
        'bandwidth': None,
    },
    'medium': {
        'id': 'medium',
        'name': 'Medium instance',
        'cpu': 3000,
        'memory': 4096,
        'disk': 500,
        'bandwidth': None,
    },
    'large': {
        'id': 'large',
        'name': 'Large instance',
        'cpu': 4000,
        'memory': 7680,
        'disk': 850,
        'bandwidth': None,
    },
    'extra-large': {
        'id': 'extra-large',
        'name': 'Extra Large instance',
        'cpu': 8000,
        'memory': 8192,
        'disk': 1690,
        'bandwidth': None,
    },
    'high-cpu-medium': {
        'id': 'high-cpu-medium',
        'name': 'High-CPU Medium instance',
        'cpu': 5000,
        'memory': 1700,
        'disk': 350,
        'bandwidth': None,
    },
    'high-cpu-extra-large': {
        'id': 'high-cpu-extra-large',
        'name': 'High-CPU Extra Large instance',
        'cpu': 20000,
        'memory': 7168,
        'disk': 1690,
        'bandwidth': None,
    },
}

# Retrieved from http://www.elastichosts.com/cloud-hosting/api
STANDARD_DRIVES = {
    '38df0986-4d85-4b76-b502-3878ffc80161': {
        'uuid': '38df0986-4d85-4b76-b502-3878ffc80161',
        'description': 'CentOS Linux 5.5',
        'size_gunzipped': '3GB',
        'supports_deployment': True,
    },
    '980cf63c-f21e-4382-997b-6541d5809629': {
        'uuid': '980cf63c-f21e-4382-997b-6541d5809629',
        'description': 'Debian Linux 5.0',
        'size_gunzipped': '1GB',
        'supports_deployment': True,
    },
    'aee5589a-88c3-43ef-bb0a-9cab6e64192d': {
        'uuid': 'aee5589a-88c3-43ef-bb0a-9cab6e64192d',
        'description': 'Ubuntu Linux 10.04',
        'size_gunzipped': '1GB',
        'supports_deployment': True,
    },
    'b9d0eb72-d273-43f1-98e3-0d4b87d372c0': {
        'uuid': 'b9d0eb72-d273-43f1-98e3-0d4b87d372c0',
        'description': 'Windows Web Server 2008',
        'size_gunzipped': '13GB',
        'supports_deployment': False,
    },
    '30824e97-05a4-410c-946e-2ba5a92b07cb': {
        'uuid': '30824e97-05a4-410c-946e-2ba5a92b07cb',
        'description': 'Windows Web Server 2008 R2',
        'size_gunzipped': '13GB',
        'supports_deployment': False,
    },
    '9ecf810e-6ad1-40ef-b360-d606f0444671': {
        'uuid': '9ecf810e-6ad1-40ef-b360-d606f0444671',
        'description': 'Windows Web Server 2008 R2 + SQL Server',
        'size_gunzipped': '13GB',
        'supports_deployment': False,
    },
    '10a88d1c-6575-46e3-8d2c-7744065ea530': {
        'uuid': '10a88d1c-6575-46e3-8d2c-7744065ea530',
        'description': 'Windows Server 2008 Standard R2',
        'size_gunzipped': '13GB',
        'supports_deployment': False,
    },
    '2567f25c-8fb8-45c7-95fc-bfe3c3d84c47': {
        'uuid': '2567f25c-8fb8-45c7-95fc-bfe3c3d84c47',
        'description': 'Windows Server 2008 Standard R2 + SQL Server',
        'size_gunzipped': '13GB',
        'supports_deployment': False,
    },
}

NODE_STATE_MAP = {
    'active': NodeState.RUNNING,
    'dead': NodeState.TERMINATED,
    'dumped': NodeState.TERMINATED,
}

# Default timeout (in seconds) for the drive imaging process
IMAGING_TIMEOUT = 10 * 60

class ElasticHostsException(Exception):
    """
    Exception class for ElasticHosts driver
    """

    def __str__(self):
        return self.args[0]

    def __repr__(self):
        return "<ElasticHostsException '%s'>" % (self.args[0])

class ElasticHostsResponse(Response):
    def success(self):
        if self.status == 401:
            raise InvalidCredsError()

        return self.status >= 200 and self.status <= 299

    def parse_body(self):
        if not self.body:
            return self.body

        try:
            data = json.loads(self.body)
        except:
            raise MalformedResponseError("Failed to parse JSON",
                                         body=self.body,
                                         driver=ElasticHostsBaseNodeDriver)

        return data

    def parse_error(self):
        error_header = self.headers.get('x-elastic-error', '')
        return 'X-Elastic-Error: %s (%s)' % (error_header, self.body.strip())

class ElasticHostsNodeSize(NodeSize):
    def __init__(self, id, name, cpu, ram, disk, bandwidth, price, driver):
        self.id = id
        self.name = name
        self.cpu = cpu
        self.ram = ram
        self.disk = disk
        self.bandwidth = bandwidth
        self.price = price
        self.driver = driver

    def __repr__(self):
        return (('<NodeSize: id=%s, name=%s, cpu=%s, ram=%s '
                 'disk=%s bandwidth=%s price=%s driver=%s ...>')
                % (self.id, self.name, self.cpu, self.ram,
                   self.disk, self.bandwidth, self.price, self.driver.name))

class ElasticHostsBaseConnection(ConnectionUserAndKey):
    """
    Base connection class for the ElasticHosts driver
    """

    host = API_ENDPOINTS[DEFAULT_ENDPOINT]['host']
    responseCls = ElasticHostsResponse

    def add_default_headers(self, headers):
        headers['Accept'] = 'application/json'
        headers['Content-Type'] = 'application/json'
        headers['Authorization'] = ('Basic %s'
                                    % (base64.b64encode('%s:%s'
                                                        % (self.user_id,
                                                           self.key))))
        return headers

class ElasticHostsBaseNodeDriver(NodeDriver):
    """
    Base ElasticHosts node driver
    """

    type = Provider.ELASTICHOSTS
    api_name = 'elastichosts'
    name = 'ElasticHosts'
    connectionCls = ElasticHostsBaseConnection
    features = {"create_node": ["generates_password"]}

    def reboot_node(self, node):
        # Reboots the node
        response = self.connection.request(
            action='/servers/%s/reset' % (node.id),
            method='POST'
        )
        return response.status == 204

    def destroy_node(self, node):
        # Kills the server immediately
        response = self.connection.request(
            action='/servers/%s/destroy' % (node.id),
            method='POST'
        )
        return response.status == 204

    def list_images(self, location=None):
        # Returns a list of available pre-installed system drive images
        images = []
        for key, value in STANDARD_DRIVES.iteritems():
            image = NodeImage(
                id=value['uuid'],
                name=value['description'],
                driver=self.connection.driver,
                extra={
                    'size_gunzipped': value['size_gunzipped']
                }
            )
            images.append(image)

        return images

    def list_sizes(self, location=None):
        sizes = []
        for key, value in INSTANCE_TYPES.iteritems():
            size = ElasticHostsNodeSize(
                id=value['id'],
                name=value['name'], cpu=value['cpu'], ram=value['memory'],
                disk=value['disk'], bandwidth=value['bandwidth'], 
                price=self._get_size_price(size_id=value['id']),
                driver=self.connection.driver
            )
            sizes.append(size)

        return sizes

    def list_nodes(self):
        # Returns a list of active (running) nodes
        response = self.connection.request(action='/servers/info').object

        nodes = []
        for data in response:
            node = self._to_node(data)
            nodes.append(node)

        return nodes

    def create_node(self, **kwargs):
        """Creates a ElasticHosts instance

        See L{NodeDriver.create_node} for more keyword args.

        @keyword    name: String with a name for this new node (required)
        @type       name: C{string}

        @keyword    smp: Number of virtual processors or None to calculate
                         based on the cpu speed
        @type       smp: C{int}

        @keyword    nic_model: e1000, rtl8139 or virtio
                               (if not specified, e1000 is used)
        @type       nic_model: C{string}

        @keyword    vnc_password: If set, the same password is also used for
                                  SSH access with user toor,
                                  otherwise VNC access is disabled and
                                  no SSH login is possible.
        @type       vnc_password: C{string}
        """
        size = kwargs['size']
        image = kwargs['image']
        smp = kwargs.get('smp', 'auto')
        nic_model = kwargs.get('nic_model', 'e1000')
        vnc_password = ssh_password = kwargs.get('vnc_password', None)

        if nic_model not in ('e1000', 'rtl8139', 'virtio'):
            raise ElasticHostsException('Invalid NIC model specified')

        # check that drive size is not smaller then pre installed image size

        # First we create a drive with the specified size
        drive_data = {}
        drive_data.update({'name': kwargs['name'],
                           'size': '%sG' % (kwargs['size'].disk)})

        response = self.connection.request(action='/drives/create',
                                           data=json.dumps(drive_data),
                                           method='POST').object

        if not response:
            raise ElasticHostsException('Drive creation failed')

        drive_uuid = response['drive']

        # Then we image the selected pre-installed system drive onto it
        response = self.connection.request(
            action='/drives/%s/image/%s/gunzip' % (drive_uuid, image.id),
            method='POST'
        )

        if response.status != 204:
            raise ElasticHostsException('Drive imaging failed')

        # We wait until the drive is imaged and then boot up the node
        # (in most cases, the imaging process shouldn't take longer
        # than a few minutes)
        response = self.connection.request(
            action='/drives/%s/info' % (drive_uuid)
        ).object
        imaging_start = time.time()
        while response.has_key('imaging'):
            response = self.connection.request(
                action='/drives/%s/info' % (drive_uuid)
            ).object
            elapsed_time = time.time() - imaging_start
            if (response.has_key('imaging')
                and elapsed_time >= IMAGING_TIMEOUT):
                raise ElasticHostsException('Drive imaging timed out')
            time.sleep(1)

        node_data = {}
        node_data.update({'name': kwargs['name'],
                          'cpu': size.cpu,
                          'mem': size.ram,
                          'ide:0:0': drive_uuid,
                          'boot': 'ide:0:0',
                          'smp': smp})
        node_data.update({'nic:0:model': nic_model, 'nic:0:dhcp': 'auto'})

        if vnc_password:
            node_data.update({'vnc:ip': 'auto', 'vnc:password': vnc_password})

        response = self.connection.request(
            action='/servers/create', data=json.dumps(node_data),
            method='POST'
        ).object

        if isinstance(response, list):
            nodes = [self._to_node(node, ssh_password) for node in response]
        else:
            nodes = self._to_node(response, ssh_password)

        return nodes

    # Extension methods
    def ex_set_node_configuration(self, node, **kwargs):
        # Changes the configuration of the running server
        valid_keys = ('^name$', '^parent$', '^cpu$', '^smp$', '^mem$',
                      '^boot$', '^nic:0:model$', '^nic:0:dhcp',
                      '^nic:1:model$', '^nic:1:vlan$', '^nic:1:mac$',
                      '^vnc:ip$', '^vnc:password$', '^vnc:tls',
                      '^ide:[0-1]:[0-1](:media)?$',
                      '^scsi:0:[0-7](:media)?$', '^block:[0-7](:media)?$')

        invalid_keys = []
        for key in kwargs.keys():
            matches = False
            for regex in valid_keys:
                if re.match(regex, key):
                    matches = True
                    break
            if not matches:
                invalid_keys.append(key)

        if invalid_keys:
            raise ElasticHostsException(
                'Invalid configuration key specified: %s'
                % (',' .join(invalid_keys))
            )

        response = self.connection.request(
            action='/servers/%s/set' % (node.id), data=json.dumps(kwargs),
            method='POST'
        )

        return (response.status == httplib.OK and response.body != '')

    def deploy_node(self, **kwargs):
        """
        Create a new node, and start deployment.

        @keyword    enable_root: If true, root password will be set to
                                 vnc_password (this will enable SSH access)
                                 and default 'toor' account will be deleted.
        @type       enable_root: C{bool}

        For detailed description and keywords args, see
        L{NodeDriver.deploy_node}.
        """
        image = kwargs['image']
        vnc_password = kwargs.get('vnc_password', None)
        enable_root = kwargs.get('enable_root', False)

        if not vnc_password:
            raise ValueError('You need to provide vnc_password argument '
                             'if you want to use deployment')

        if (image in STANDARD_DRIVES
            and STANDARD_DRIVES[image]['supports_deployment']):
            raise ValueError('Image %s does not support deployment'
                             % (image.id))

        if enable_root:
            script = ("unset HISTFILE;"
                      "echo root:%s | chpasswd;"
                      "sed -i '/^toor.*$/d' /etc/passwd /etc/shadow;"
                      "history -c") % vnc_password
            root_enable_script = ScriptDeployment(script=script,
                                                  delete=True)
            deploy = kwargs.get('deploy', None)
            if deploy:
                if (isinstance(deploy, ScriptDeployment)
                    or isinstance(deploy, SSHKeyDeployment)):
                    deployment = MultiStepDeployment([deploy,
                                                      root_enable_script])
                elif isinstance(deploy, MultiStepDeployment):
                    deployment = deploy
                    deployment.add(root_enable_script)
            else:
                deployment = root_enable_script

            kwargs['deploy'] = deployment

        if not kwargs.get('ssh_username', None):
            kwargs['ssh_username'] = 'toor'

        return super(ElasticHostsBaseNodeDriver, self).deploy_node(**kwargs)

    def ex_shutdown_node(self, node):
        # Sends the ACPI power-down event
        response = self.connection.request(
            action='/servers/%s/shutdown' % (node.id),
            method='POST'
        )
        return response.status == 204

    def ex_destroy_drive(self, drive_uuid):
        # Deletes a drive
        response = self.connection.request(
            action='/drives/%s/destroy' % (drive_uuid),
            method='POST'
        )
        return response.status == 204

    # Helper methods
    def _to_node(self, data, ssh_password=None):
        try:
            state = NODE_STATE_MAP[data['status']]
        except KeyError:
            state = NodeState.UNKNOWN

        if isinstance(data['nic:0:dhcp'], list):
            public_ip = data['nic:0:dhcp']
        else:
            public_ip = [data['nic:0:dhcp']]

        extra = {'cpu': data['cpu'],
                 'smp': data['smp'],
                 'mem': data['mem'],
                 'started': data['started']}

        if data.has_key('vnc:ip') and data.has_key('vnc:password'):
            extra.update({'vnc_ip': data['vnc:ip'],
                          'vnc_password': data['vnc:password']})

        if ssh_password:
            extra.update({'password': ssh_password})

        node = Node(id=data['server'], name=data['name'], state=state,
                    public_ip=public_ip, private_ip=None,
                    driver=self.connection.driver,
                    extra=extra)

        return node

class ElasticHostsUK1Connection(ElasticHostsBaseConnection):
    """
    Connection class for the ElasticHosts driver for
    the London Peer 1 end-point
    """

    host = API_ENDPOINTS['uk-1']['host']

class ElasticHostsUK1NodeDriver(ElasticHostsBaseNodeDriver):
    """
    ElasticHosts node driver for the London Peer 1 end-point
    """
    connectionCls = ElasticHostsUK1Connection

class ElasticHostsUK2Connection(ElasticHostsBaseConnection):
    """
    Connection class for the ElasticHosts driver for
    the London Bluesquare end-point
    """
    host = API_ENDPOINTS['uk-2']['host']

class ElasticHostsUK2NodeDriver(ElasticHostsBaseNodeDriver):
    """
    ElasticHosts node driver for the London Bluesquare end-point
    """
    connectionCls = ElasticHostsUK2Connection

class ElasticHostsUS1Connection(ElasticHostsBaseConnection):
    """
    Connection class for the ElasticHosts driver for
    the San Antonio Peer 1 end-point
    """
    host = API_ENDPOINTS['us-1']['host']

class ElasticHostsUS1NodeDriver(ElasticHostsBaseNodeDriver):
    """
    ElasticHosts node driver for the San Antonio Peer 1 end-point
    """
    connectionCls = ElasticHostsUS1Connection
