#!/usr/bin/env python
#==============================================================================
# Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Amazon Software License (the "License"). You may not use
# this file except in compliance with the License. A copy of the License is
# located at
#
#             http://aws.amazon.com/asl/
#
# or in the "license" file accompanying this file. This file is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or
# implied. See the License for the specific language governing permissions
# and limitations under the License.
#==============================================================================

from __future__ import with_statement
from os import environ
import sys
import logging
from boto.s3.connection import OrdinaryCallingFormat
from boto.s3.connection import S3Connection
from boto.s3.key import Key
import random, time
import os
import requests

try:
    import simplejson as json
except ImportError:
    import json

# for compatibility with older versions of Boto
# this has to be set explicitly for FRA to work correctly (SigV4)
environ['S3_USE_SIGV4'] = 'true'

logfile = '/var/log/eb-version-deployment.log'
sys.stdout = open(logfile, 'a')
sys.stderr = open(logfile, 'a')

logger = logging.getLogger('version_deployment')
hdlr = logging.FileHandler(logfile)
formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
hdlr.setFormatter(formatter)
logger.addHandler(hdlr) 
logger.setLevel(logging.INFO)

def err_exit(inst):
    logger.exception(u'Exception in downloading source bundle. Exception message: %s'%str(inst))
    print 'Exception in downloading source bundle. Exception message: %s'%str(inst)
    exit(1)

def fixed_backoff():
    while True:
        yield 5
    
def retry_on_failure(max_tries=20, delay_generator=fixed_backoff()):
    def decorate(function):
        def retry(*args, **kwargs):
            count = 1
            error = None
            for i in delay_generator:
                # if max_tries is None then retry infinitely
                if max_tries and count > max_tries:
                    logger.error(u'Reached the max limit of tries: {0}. '\
                              'Giving up and returning last exception received: {1}'\
                              .format(max_tries, error))
                    raise error
                count += 1
                try:
                    return function(*args, **kwargs)
                except Exception as e:
                    logger.exception(u'Encountered exception: "{0}"'.format(e))
                    error = e
                    logger.info(u'Sleeping for %f seconds before retrying', i)
                    time.sleep(i)                    
        return retry
    return decorate
                        
@retry_on_failure()
def download_source_bundle(json_data, env_id, dest_source_bundle_file_loc, bucket):

    version_s3_key_name = None
    runtimesources_value = json_data['RuntimeSources']
    for app_name, version_properties in runtimesources_value.iteritems():
        for version_label, version_info in version_properties.iteritems():
            version_s3_key_name = 'resources/environments/%s/_runtime/_versions/%s/%s'%(env_id, app_name, version_label)
            version_s3_key = bucket.get_key(version_s3_key_name)
            version_s3_key.get_contents_to_filename(dest_source_bundle_file_loc)
            try:
                logger.info('Downloaded version label %s from s3 key %s' %(version_label if isinstance(version_label, str) else version_label.encode('utf-8'), version_s3_key))
            except:
                pass
            return True
           
    # If version_s3_key_name is None, means environment is running with no version
    # It should run the sample application from default value of AppSource parameter
    if version_s3_key_name is None:
            
        app_source_config_location = '/opt/elasticbeanstalk/deploy/configuration/appsourceurl'
            
        # Read the app source file
        with open(app_source_config_location) as app_source_file:
            app_source_file_data = app_source_file.read()
                 
        # Read the app source from the json file     
        app_source_json_data = json.loads(app_source_file_data)
        app_source_url = app_source_json_data['url']

        # Read the source bundle from app source url            
        response = requests.get(app_source_url)
        with open(dest_source_bundle_file_loc, 'wb') as fd:
            for chunk in response.iter_content(64):
                fd.write(chunk)
                
        logger.info(u'Downloaded sample application to the environment.' )
                
    return True

def get_host_by_region(region):
    if region == 'us-east-1':
        return 's3.amazonaws.com'

    if region == 'eu-central-1':
        return 's3.eu-central-1.amazonaws.com'

    if region == 'cn-north-1':
        return 's3.cn-north-1.amazonaws.com.cn'

    return 's3-%s.amazonaws.com' % region

def read_manifest_download_source_bundle(bucket_name, region, env_id, temp_manifest_file) :
    # As of now, we support only one application version, need to change this if we start supporting multiple versions
    try:
        dest_source_bundle_file_loc = '/opt/elasticbeanstalk/deploy/appsource/source_bundle'

        conn = S3Connection(host = get_host_by_region(region), calling_format = OrdinaryCallingFormat())
        bucket = conn.get_bucket(bucket_name,validate=False)
        
        with open(temp_manifest_file) as manifest_file:
            manifest_data = manifest_file.read()
            
        json_data = json.loads(manifest_data)
        
        # Retry this call on the instance to handle eventual consistency
        download_source_bundle(json_data, env_id, dest_source_bundle_file_loc, bucket)
        
    except Exception, inst :
        logger.error(u'Exception in downloading the source bundle from bucket %s' %bucket_name)
        err_exit(inst)

@retry_on_failure()
def download_manifest_file_retry(bucket_name, latest_version_manifest_file_s3_key_name, temp_manifest_file, region) :
        conn = S3Connection(host = get_host_by_region(region), calling_format = OrdinaryCallingFormat())
        s3bucket = conn.get_bucket(bucket_name,validate=False)
        latest_version_manifest_file_s3_key = Key(bucket=s3bucket, name=latest_version_manifest_file_s3_key_name)
        latest_version_manifest_file_s3_key.get_contents_to_filename(temp_manifest_file)
        logger.info(u'Downloaded the manifest file to %s'%temp_manifest_file)


def download_manifest_file(bucket_name, latest_version_manifest_file_s3_key_name, temp_manifest_file, region) :
    try:
        # Retry this call on the instance to handle eventual consistency
        download_manifest_file_retry(bucket_name, latest_version_manifest_file_s3_key_name, temp_manifest_file, region)
    except Exception, inst :
        logger.error(u'Exception in downloading the manifest file from bucket %s and key %s' %(bucket_name, latest_version_manifest_file_s3_key_name))
        err_exit(inst)

@retry_on_failure()       
def get_latest_version_manifest_file_s3_key_retry(bucket_name, prefix, region):
    conn = S3Connection(host = get_host_by_region(region), calling_format = OrdinaryCallingFormat())
    bucket = conn.get_bucket(bucket_name,validate=False)
        
    key = None
    timestamp = None
    
    for count in range(0, 3):
        s3keys = bucket.list(prefix)
        for s3key in s3keys:
            if s3key.last_modified > timestamp and not s3key.name.endswith('/'):
                file_name = s3key.name.rsplit('/', 1)[1]
                if file_name.startswith('manifest'):
                    timestamp = s3key.last_modified
                    key = s3key
    
    if key:
        logger.info(u'Found the latest version manifest file %s from bucket %s and prefix %s'%(key, bucket_name, prefix))
    else:
        logger.error(u'Exception in getting the location of latest version manifest file from bucket %s and prefix %s' %(bucket_name, prefix))
        # raise exception so that it gets retried
        raise Exception
            
    return key
 
def get_latest_version_manifest_file_s3_key(bucket_name, version_manifest_dir_prefix, region): 
    try:
        # Retry this call on the instance to handle eventual consistency
        key = get_latest_version_manifest_file_s3_key_retry(bucket_name, version_manifest_dir_prefix, region)
        return key      
    except Exception, inst:
        logger.error(u'Exception in getting the location of latest version manifest file from bucket %s and prefix %s' %(bucket_name, version_manifest_dir_prefix))
        err_exit(inst)
            

def read_stack_properties_file() :
    stack_props = {}
    with open('/etc/elasticbeanstalk/.aws-eb-stack.properties') as stack_props_file:
        for line in stack_props_file:
            name, var = line.partition("=")[::2]
            stack_props[name.strip()] = var.strip()
        
    return stack_props
               
def main():
    
    # Get the customer service bucket, region and environment reference id from stack properties file
    myvars = read_stack_properties_file()
    bucket_name = myvars['environment_bucket']
    region = myvars['region']
    env_id = myvars['environment_id']
    
    # create the version manifest file prefix
    version_manifest_dir_prefix = 'resources/environments/%s/_runtime/versions/'%env_id
    
    # Get the manifest file, first check in the command data otherwise get the latest
    if environ.has_key('EB_COMMAND_DATA'):
        manifest_file_name = environ['EB_COMMAND_DATA'].strip()
        latest_version_manifest_file_s3_key_name = '%s%s'%(version_manifest_dir_prefix, manifest_file_name)
        logger.info(u'Version manifest file name already known. The latest version manifest file key is %s'%latest_version_manifest_file_s3_key_name)
    else:
        latest_version_manifest_file_s3_key = get_latest_version_manifest_file_s3_key(bucket_name, version_manifest_dir_prefix, region)
        latest_version_manifest_file_s3_key_name = latest_version_manifest_file_s3_key.name
    
    # Read manifest file
    # role should have permissions to get the manifest file object
    temp_manifest_file = '/tmp/version_file_manifest'
    download_manifest_file(bucket_name, latest_version_manifest_file_s3_key_name, temp_manifest_file, region)
    
    # Download the source bundle to correct location
    # role should have permissions to download the source bundle
    read_manifest_download_source_bundle(bucket_name, region, env_id, temp_manifest_file)
    
    # clean up the temporary manifest file
    if os.path.exists(temp_manifest_file) :
        os.remove(temp_manifest_file)
            
if __name__ == '__main__':
    
    main()
