#!/usr/bin/python
# 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.

# Version @VERSION@
#
# A plugin for executing script needed by cloud  stack
from __future__ import with_statement

from copy import copy
from datetime import datetime
from httplib import *
from string import join

import os
import sys
import time
import hashlib
import base64
import hmac
import traceback
import urllib2

import XenAPIPlugin
sys.path.extend(["/opt/xensource/sm/"])
import util

NULL = 'null'

# Value conversion utility functions ...


def to_none(value):
    return value if value is not None and value.strip() != NULL else None


def to_bool(value):
    return True if to_none(value) in ['true', 'True', None] else False


def to_integer(value, default):
    return int(value) if to_none(value) is not None else default


def optional_str_value(value, default):
    return value if is_not_blank(value) else default


def is_not_blank(value):
    return True if to_none(value) is not None and value.strip != '' else False


def get_optional_key(map, key, default=''):
    return map[key] if key in map else default


def log(message):
    util.SMlog('#### VMOPS %s ####' % message)


def echo(fn):
    def wrapped(*v, **k):
        name = fn.__name__
        log("enter %s ####" % name)
        res = fn(*v, **k)
        log("exit %s with result %s" % name, res)
        return res
    return wrapped


def require_str_value(value, error_message):

    if is_not_blank(value):
        return value

    raise ValueError(error_message)


def retry(max_attempts, fn):

    attempts = 1
    while attempts <= max_attempts:
        log("Attempting execution {0}/{1} of {2}".
            format(attempts, max_attempts, fn.__name__))
        try:
            return fn()
        except:
            if (attempts >= max_attempts):
                raise
            attempts = attempts + 1


def compute_md5(filename, buffer_size=8192):

    hasher = hashlib.md5()

    with open(filename, 'rb') as file:
        data = file.read(buffer_size)
        while data != "":
            hasher.update(data)
            data = file.read(buffer_size)

    return base64.encodestring(hasher.digest())[:-1]


class S3Client(object):

    DEFAULT_END_POINT = 's3.amazonaws.com'
    DEFAULT_CONNECTION_TIMEOUT = 50000
    DEFAULT_SOCKET_TIMEOUT = 50000
    DEFAULT_MAX_ERROR_RETRY = 3

    HEADER_CONTENT_MD5 = 'Content-MD5'
    HEADER_CONTENT_TYPE = 'Content-Type'
    HEADER_CONTENT_LENGTH = 'Content-Length'

    def __init__(self, access_key, secret_key, end_point=None,
                 https_flag=None, connection_timeout=None, socket_timeout=None,
                 max_error_retry=None):

        self.access_key = require_str_value(
            access_key, 'An access key must be specified.')
        self.secret_key = require_str_value(
            secret_key, 'A secret key must be specified.')
        self.end_point = optional_str_value(end_point, self.DEFAULT_END_POINT)
        self.https_flag = to_bool(https_flag)
        self.connection_timeout = to_integer(
            connection_timeout, self.DEFAULT_CONNECTION_TIMEOUT)
        self.socket_timeout = to_integer(
            socket_timeout, self.DEFAULT_SOCKET_TIMEOUT)
        self.max_error_retry = to_integer(
            max_error_retry, self.DEFAULT_MAX_ERROR_RETRY)

    def build_canocialized_resource(self, bucket, key):

        return '/{bucket}/{key}'.format(bucket=bucket, key=key)

    def noop_send_body():
        pass

    def noop_read(response):
        return response.read()

    def do_operation(
        self, method, bucket, key, input_headers={},
            fn_send_body=noop_send_body, fn_read=noop_read):

        headers = copy(input_headers)
        headers['Expect'] = '100-continue'

        uri = self.build_canocialized_resource(bucket, key)
        signature, request_date = self.sign_request(method, uri, headers)
        headers['Authorization'] = "AWS {0}:{1}".format(
            self.access_key, signature)
        headers['Date'] = request_date

        connection = HTTPSConnection(self.end_point) \
            if self.https_flag else HTTPConnection(self.end_point)
        connection.timeout = self.socket_timeout

        def perform_request():

            connection.request(method, uri, fn_send_body(), headers)
            response = connection.getresponse()
            log("Sent {0} request to {1} {2} with headers {3}.  \
                Got response status {4}: {5}".
                format(method, self.end_point, uri, headers,
                response.status, response.reason))
            return fn_read(response)

        try:
            return retry(self.max_error_retry, perform_request)
        finally:
            connection.close()

    '''
    See http://bit.ly/MMC5de for more information regarding the creation of
    AWS authorization tokens and header signing
    '''
    def sign_request(self, operation, canocialized_resource, headers):

        request_date = datetime.utcnow(
        ).strftime('%a, %d %b %Y %H:%M:%S +0000')

        content_hash = get_optional_key(headers, self.HEADER_CONTENT_MD5)
        content_type = get_optional_key(headers, self.HEADER_CONTENT_TYPE)

        string_to_sign = join(
            [operation, content_hash, content_type, request_date,
                canocialized_resource], '\n')

        signature = base64.encodestring(
            hmac.new(self.secret_key, string_to_sign.encode('utf8'),
                     hashlib.sha1).digest())[:-1]

        return signature, request_date

    def put(self, bucket, key, src_filename):

        headers = {
            self.HEADER_CONTENT_MD5: compute_md5(src_filename),
            self.HEADER_CONTENT_TYPE: 'application/octet-stream',
            self.HEADER_CONTENT_LENGTH: os.stat(src_filename).st_size,
        }

        def send_body():
            return open(src_filename, 'rb')

        self.do_operation('PUT', bucket, key, headers, send_body)

    def get(self, bucket, key, target_filename):

        def read(response):

            with open(target_filename, 'wb') as file:
                while True:
                    block = response.read(8192)
                    if not block:
                        break
                        file.write(block)

        return self.do_operation('GET', bucket, key, fn_read=read)

    def delete(self, bucket, key):

        return self.do_operation('DELETE', bucket, key)


def parseArguments(args):

    # The keys in the args map will correspond to the properties defined on
    # the com.cloud.utils.S3Utils#ClientOptions interface
    client = S3Client(
        args['accessKey'], args['secretKey'], args['endPoint'],
        args['isHttps'], args['connectionTimeout'], args['socketTimeout'])

    operation = args['operation']
    bucket = args['bucket']
    key = args['key']
    filename = args['filename']

    if is_blank(operation):
        raise ValueError('An operation must be specified.')

    if is_blank(bucket):
        raise ValueError('A bucket must be specified.')

    if is_blank(key):
        raise ValueError('A value must be specified.')

    if is_blank(filename):
        raise ValueError('A filename must be specified.')

    return client, operation, bucket, key, filename


@echo
def s3(session, args):

    client, operation, bucket, key, filename = parseArguments(args)

    try:

        if operation == 'put':
            client.put(bucket, key, filename)
        elif operation == 'get':
            client.get(bucket, key, filename)
        elif operation == 'delete':
            client.delete(bucket, key, filename)
        else:
            raise RuntimeError(
                "S3 plugin does not support operation {0}.".format(operation))

        return 'true'

    except:
        log("Operation {0} on file {1} from/in bucket {2} key {3}.".format(
            operation, filename, bucket, key))
        log(traceback.format_exc())
        return 'false'

if __name__ == "__main__":
    XenAPIPlugin.dispatch({"s3": s3})
