# Software License Agreement (BSD License)
#
# Copyright (c) 2009-2011, Eucalyptus Systems, Inc.
# All rights reserved.
#
# Redistribution and use of this software in source and binary forms, with or
# without modification, are permitted provided that the following conditions
# are met:
#
#   Redistributions of source code must retain the above
#   copyright notice, this list of conditions and the
#   following disclaimer.
#
#   Redistributions in binary form must reproduce the above
#   copyright notice, this list of conditions and the
#   following disclaimer in the documentation and/or other
#   materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Author: Neil Soman neil@eucalyptus.com
#         Mitch Garnaat mgarnaat@eucalyptus.com

import boto
import getopt
import os
import socket
import sys
import textwrap
import urlparse
import euca2ools
import euca2ools.utils
import euca2ools.exceptions
import euca2ools.nc.auth
import euca2ools.nc.connection
from boto.ec2.regioninfo import RegionInfo
from boto.s3.connection import OrdinaryCallingFormat
from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType
from boto.roboto.param import Param

SYSTEM_EUCARC_PATH = os.path.join('/etc', 'euca2ools', 'eucarc')

import bdb
import traceback
try:
    import epdb as debugger
except ImportError:
    import pdb as debugger

def euca_except_hook(debugger_flag, debug_flag):
    def excepthook(typ, value, tb):
        if typ is bdb.BdbQuit:
            sys.exit(1)
        sys.excepthook = sys.__excepthook__

        if debugger_flag and sys.stdout.isatty() and sys.stdin.isatty():
            if debugger.__name__ == 'epdb':
                debugger.post_mortem(tb, typ, value)
            else:
                debugger.post_mortem(tb)
        elif debug_flag:
            print >> sys.stderr, traceback.print_tb(tb)
            sys.exit(1)
        else:
            print >> sys.stderr, value
            sys.exit(1)

    return excepthook

class EucaCommand(object):

    Description = 'Base class'
    StandardOptions = [Param(name='access_key',
                             short_name='a', long_name='access-key',
                             doc="User's Access Key ID.",
                             optional=True),
                       Param(name='secret_key',
                             short_name='s', long_name='secret-key',
                             doc="User's Secret Key.",
                             optional=True),
                       Param(name='config_path',
                             short_name=None, long_name='config',
                             doc="""Read credentials and cloud settings
                             from the specified config file (defaults to
                             $HOME/.eucarc or /etc/euca2ools/eucarc).""",
                             optional=True),
                       Param(short_name=None, long_name='debug',
                             doc='Turn on debugging output.',
                             optional=True, ptype='boolean'),
                       Param(short_name=None, long_name='debugger',
                             doc='Enable interactive debugger on error',
                             optional=True, ptype='boolean'),
                       Param(short_name='h', long_name='help',
                             doc='Display this help message.',
                             optional=True, ptype='boolean'),
                       Param(name='region_name',
                             short_name=None, long_name='region',
                             doc='region to direct requests to',
                             optional=True),
                       Param(short_name='U', long_name='url',
                             doc='URL of the Cloud to connect to.',
                             optional=True),
                       Param(short_name=None, long_name='version',
                             doc='Display the version of this tool.',
                             optional=True, ptype='boolean'),
                       Param(long_name='euca-auth',
                             doc='Use NC authentication mode',
                             optional=True, ptype='boolean')]
    Options = []
    Args = []
    Filters = []
    APIVersion = '2009-11-30'

    def __init__(self, is_euca=False, debug=False):
        self.access_key_short_name = '-a'
        self.secret_key_short_name = '-s'
        self.ec2_user_access_key = None
        self.ec2_user_secret_key = None
        self.url = None
        self.filters = {}
        self.region_name = None
        self.region = RegionInfo()
        self.config_file_path = None
        self.is_secure = True
        self.port = 443
        self.service_path = '/'
        self.is_euca = is_euca
        self.euca_cert_path = None
        self.euca_private_key_path = None
        self.debug = 0
        self.debugger = False
        self.set_debug(debug)
        self.cmd_name = os.path.basename(sys.argv[0])
        self.check_for_conflict()
        self.process_cli_args()
        self.setup_environ()
        # h = NullHandler()
        # logging.getLogger('boto').addHandler(h)

    def set_debug(self, debug=False):
        if debug:
            boto.set_stream_logger('euca2ools')
            self.debug = 2

    def process_cli_args(self):
        try:
            (opts, args) = getopt.gnu_getopt(sys.argv[1:],
                                             self.short_options(),
                                             self.long_options())
        except getopt.GetoptError, e:
            print >> sys.stderr, e
            sys.exit(1)
        for (name, value) in opts:
            if name in ('-h', '--help'):
                self.usage()
                sys.exit()
            elif name == '--version':
                self.version()
            elif name == '--debug':
                self.set_debug(True)
            elif name == '--debugger':
                self.debugger = True
            elif name in (self.access_key_short_name, '--access-key'):
                self.ec2_user_access_key = value
            elif name in (self.secret_key_short_name, '--secret-key'):
                self.ec2_user_secret_key = value
            elif name in ('-U', '--url'):
                self.url = value
            elif name == '--region':
                self.region_name = value
            elif name == '--config':
                self.config_file_path = value
            elif name == '--euca-auth':
                self.is_euca = True
            elif name == '--filter':
                try:
                    name, value = value.split('=')
                except ValueError:
                    msg = 'Filters must be of the form name=value'
                    self.display_error_and_exit(msg)
                self.filters[name] = value
            else:
                option = self.find_option(name)
                if option:
                    try:
                        value = option.convert(value)
                    except:
                        msg = '%s should be of type %s' % (option.long_name,
                                                           option.ptype)
                        self.display_error_and_exit(msg)
                    if option.choices:
                        if value not in option.choices:
                            msg = 'Value must be one of: %s' % '|'.join(option.choices)
                            self.display_error_and_exit(msg)
                    if option.cardinality in ('*', '+'):
                        if not hasattr(self, option.name):
                            setattr(self, option.name, [])
                        getattr(self, option.name).append(value)
                    else:
                        setattr(self, option.name, value)
        self.handle_defaults()
        self.check_required_options()

        for arg in self.Args:
            if not arg.optional and len(args)==0:
                self.usage()
                msg = 'Argument (%s) was not provided' % arg.name
                self.display_error_and_exit(msg)
            if arg.cardinality in ('*', '+'):
                setattr(self, arg.name, args)
            elif arg.cardinality == 1:
                if len(args) == 0 and arg.optional:
                    continue
                try:
                    value = arg.convert(args[0])
                except:
                    msg = '%s should be of type %s' % (arg.name,
                                                       arg.ptype)
                setattr(self, arg.name, value)
                if len(args) > 1:
                    msg = 'Only 1 argument (%s) permitted' % arg.name
                    self.display_error_and_exit(msg)

        sys.excepthook = euca_except_hook(self.debugger, self.debug)


    def check_for_conflict(self):
        for option in self.Options:
            if option.short_name == 'a' or option.short_name == 's':
                self.access_key_short_name = '-A'
                self.secret_key_short_name = '-S'
                opt = self.find_option('--access-key')
                opt.short_name = 'A'
                opt = self.find_option('--secret-key')
                opt.short_name = 'S'

    def find_option(self, op_name):
        for option in self.StandardOptions+self.Options:
            if option.synopsis_short_name == op_name or option.synopsis_long_name == op_name:
                return option
        return None

    def short_options(self):
        s = ''
        for option in self.StandardOptions + self.Options:
            if option.short_name:
                s += option.getopt_short_name
        return s

    def long_options(self):
        l = []
        for option in self.StandardOptions+self.Options:
            if option.long_name:
                l.append(option.getopt_long_name)
        if self.Filters:
            l.append('filter=')
        return l

    def required(self):
        return [ opt for opt in self.StandardOptions+self.Options if not opt.optional ]

    def required_args(self):
        return [ arg for arg in self.Args if not arg.optional ]

    def optional(self):
        return [ opt for opt in self.StandardOptions+self.Options if opt.optional ]

    def optional_args(self):
        return [ arg for arg in self.Args if arg.optional ]

    def handle_defaults(self):
        for option in self.Options+self.Args:
            if not hasattr(self, option.name):
                value = option.default
                if value is None and option.cardinality in ('+', '*'):
                    value = []
                elif value is None and option.ptype == 'boolean':
                    value = False
                elif value is None and option.ptype == 'integer':
                    value = 0
                setattr(self, option.name, value)

    def check_required_options(self):
        missing = []
        for option in self.required():
            if not hasattr(self, option.name) or getattr(self, option.name) is None:
                missing.append(option.long_name)
        if missing:
            msg = 'These required options are missing: %s' % ','.join(missing)
            self.display_error_and_exit(msg)

    def version(self):
        print 'euca2ools %s (%s)' % (euca2ools.__version__, euca2ools.__codename__)
        if os.path.isfile('/etc/eucalyptus/eucalyptus-version'):
            with open('/etc/eucalyptus/eucalyptus-version') as version_file:
                print 'eucalyptus %s' % version_file.readline().strip()
        sys.exit(0)

    def param_usage(self, plist, label, n=30):
        nn = 80 - n - 4
        if plist:
            print '\n%s' % label
            for opt in plist:
                names = []
                if opt.short_name:
                    names.append(opt.synopsis_short_name)
                if opt.long_name:
                    names.append(opt.synopsis_long_name)
                if not names:
                    names.append(opt.name)
                doc = textwrap.dedent(opt.doc)
                doclines = textwrap.wrap(doc, nn)
                if opt.choices:
                    vv = 'Valid Values: %s' % '|'.join(opt.choices)
                    doclines += textwrap.wrap(vv, nn)
                if doclines:
                    print '    %s%s' % (','.join(names).ljust(n), doclines[0])
                    for line in doclines[1:]:
                        print '%s%s' % (' '*(n+4), line)

    def filter_usage(self, n=30):
        if self.Filters:
            nn = 80 - n - 4
            print '\nAVAILABLE FILTERS'
            for filter in self.Filters:
                doc = textwrap.dedent(filter.doc)
                doclines = textwrap.wrap(doc, nn, fix_sentence_endings=True)
                print '    %s%s' % (filter.name.ljust(n), doclines[0])
                for line in doclines[1:]:
                    print '%s%s' % (' '*(n+4), line)
                

    def option_synopsis(self, options):
        s = ''
        for option in options:
            names = []
            if option.short_name:
                names.append(option.synopsis_short_name)
            if option.long_name:
                names.append(option.synopsis_long_name)
            if option.optional:
                s += '['
            s += ', '.join(names)
            if option.ptype != 'boolean':
                if option.metavar:
                    n = option.metavar
                elif option.name:
                    n = option.name
                else:
                    n = option.long_name
                s += ' <%s> ' % n
            if option.optional:
                s += ']'
        return s

    def synopsis(self):
        s = '%s ' % self.cmd_name
        n = len(s) + 1
        t = ''
        t += self.option_synopsis(self.required())
        t += self.option_synopsis(self.optional())
        if self.Filters:
            t += ' [--filter name=value]'
        if self.Args:
            t += ' '
            arg_names = []
            for arg in self.Args:
                name = arg.name
                if arg.optional:
                    name = '[ %s ]' % name
                arg_names.append(name)
            t += ' '.join(arg_names)
        lines = textwrap.wrap(t, 80-n)
        print s, lines[0]
        for line in lines[1:]:
            print '%s%s' % (' '*n, line)
                
    def usage(self):
        print '%s\n' % self.Description
        self.synopsis()
        self.param_usage(self.required()+self.required_args(),
                         'REQUIRED PARAMETERS')
        self.param_usage(self.optional()+self.optional_args(),
                         'OPTIONAL PARAMETERS')
        self.filter_usage()

    def display_error_and_exit(self, exc):
        try:
            print >> sys.stderr, '%s: %s' % (exc.error_code, exc.error_message)
        except:
            print >> sys.stderr, '%s' % exc
        finally:
            sys.exit(1)

    def error_exit(self):
        sys.exit(1)

    def setup_environ(self):
        envlist = ('EC2_ACCESS_KEY', 'EC2_SECRET_KEY',
                   'S3_URL', 'EC2_URL', 'EC2_CERT', 'EC2_PRIVATE_KEY',
                   'EUCALYPTUS_CERT', 'EC2_USER_ID',
                   'EUCA_CERT', 'EUCA_PRIVATE_KEY')
        self.environ = {}
        user_eucarc = None
        if 'HOME' in os.environ:
            user_eucarc = os.path.join(os.getenv('HOME'), '.eucarc')
        read_config = False
        if self.config_file_path \
            and os.path.exists(self.config_file_path):
            read_config = self.config_file_path
        elif user_eucarc is not None and os.path.exists(user_eucarc):
            if os.path.isdir(user_eucarc):
                user_eucarc = os.path.join(user_eucarc, 'eucarc')
                if os.path.isfile(user_eucarc):
                    read_config = user_eucarc
            elif os.path.isfile(user_eucarc):
                read_config = user_eucarc
        elif os.path.exists(SYSTEM_EUCARC_PATH):
            read_config = SYSTEM_EUCARC_PATH
        if read_config:
            euca2ools.utils.parse_config(read_config, self.environ, envlist)
        else:
            for v in envlist:
                self.environ[v] = os.getenv(v)

    def get_environ(self, name):
        if self.environ.has_key(name):
            value = self.environ[name]
            if value:
                return self.environ[name]
        msg = 'Environment variable: %s not found' % name
        self.display_error_and_exit(msg)

    def get_credentials(self):
        if self.is_euca:
            if not self.euca_cert_path:
                self.euca_cert_path = self.environ['EUCA_CERT']
                if not self.euca_cert_path:
                    print >> sys.stderr, 'EUCA_CERT variable must be set.'
                    raise euca2ools.exceptions.ConnectionFailed
            if not self.euca_private_key_path:
                self.euca_private_key_path = self.environ['EUCA_PRIVATE_KEY']
                if not self.euca_private_key_path:
                    print >> sys.stderr, 'EUCA_PRIVATE_KEY variable must be set.'
                    raise euca2ools.exceptions.ConnectionFailed
        if not self.ec2_user_access_key:
            self.ec2_user_access_key = self.environ['EC2_ACCESS_KEY']
            if not self.ec2_user_access_key:
                print >> sys.stderr, 'EC2_ACCESS_KEY environment variable must be set.'
                raise euca2ools.exceptions.ConnectionFailed

        if not self.ec2_user_secret_key:
            self.ec2_user_secret_key = self.environ['EC2_SECRET_KEY']
            if not self.ec2_user_secret_key:
                print >> sys.stderr, 'EC2_SECRET_KEY environment variable must be set.'
                raise euca2ools.exceptions.ConnectionFailed

    def get_connection_details(self):
        self.port = None
        self.service_path = '/'
        
        rslt = urlparse.urlparse(self.url)
        if rslt.scheme == 'https':
            self.is_secure = True
        else:
            self.is_secure = False

        self.host = rslt.netloc
        l = self.host.split(':')
        if len(l) > 1:
            self.host = l[0]
            self.port = int(l[1])

        if rslt.path:
            self.service_path = rslt.path

    def make_s3_connection(self):
        if not self.url:
            self.url = self.environ['S3_URL']
            if not self.url:
                self.url = \
                    'http://localhost:8773/services/Walrus'
                print >> sys.stderr, 'S3_URL not specified. Trying %s' \
                    % self.url

        self.get_connection_details()

        if self.is_euca:
            return euca2ools.nc.connection.EucaConnection(
                private_key_path=self.euca_private_key_path,
                cert_path=self.euca_cert_path,
                aws_access_key_id=self.ec2_user_access_key,
                aws_secret_access_key=self.ec2_user_secret_key,
                is_secure=self.is_secure, debug=self.debug,
                host=self.host,
                port=self.port,
                path=self.service_path)
        else:
            return boto.connect_s3(
                aws_access_key_id=self.ec2_user_access_key,
                aws_secret_access_key=self.ec2_user_secret_key,
                is_secure=self.is_secure, debug=self.debug,
                host=self.host,
                port=self.port,
                calling_format=OrdinaryCallingFormat(),
                path=self.service_path)

    def make_ec2_connection(self):
        if self.region_name:
            self.region.name = self.region_name
            try:
                self.url = self.get_endpoint_url(self.region.name)
            except KeyError, err:
                self.display_error_and_exit(err.message)
        elif not self.url:
            self.url = self.environ['EC2_URL']
            if not self.url:
                self.url = \
                    'http://localhost:8773/services/Eucalyptus'
                print >> sys.stderr, 'EC2_URL not specified. Trying %s' \
                    % self.url

        if not self.region.endpoint:
            self.get_connection_details()
            self.region.endpoint = self.host
        if not self.region.name:
            self.region.name = 'eucalyptus'

        return boto.connect_ec2(aws_access_key_id=self.ec2_user_access_key,
                                aws_secret_access_key=self.ec2_user_secret_key,
                                is_secure=self.is_secure,
                                debug=self.debug,
                                region=self.region,
                                port=self.port,
                                path=self.service_path,
                                api_version=self.APIVersion)
    
    def make_connection(self, conn_type='ec2'):
        self.get_credentials()
        if conn_type == 's3':
            conn = self.make_s3_connection()
        elif conn_type == 'ec2':
            conn = self.make_ec2_connection()
        else:
            conn = None
        return conn

    def make_connection_cli(self, conn_type='ec2'):
        """
        This just wraps up the make_connection call with appropriate
        try/except logic to print out an error message and exit if
        a EucaError is encountered.  This keeps the try/except logic
        out of all the command files.
        """
        try:
            conn = self.make_connection(conn_type)
            if not conn:
                msg = 'Unknown connection type: %s' % conn_type
                self.display_error_and_exit(msg)
            return conn
        except euca2ools.exceptions.EucaError, ex:
            self.display_error_and_exit(ex)

    def make_request_cli(self, connection, request_name, **params):
        """
        This provides a simple
        This just wraps up the make_connection call with appropriate
        try/except logic to print out an error message and exit if
        a EucaError is encountered.  This keeps the try/except logic
        out of all the command files.
        """
        try:
            if self.filters:
                params['filters'] = self.filters
            method = getattr(connection, request_name)
        except AttributeError:
            print >> sys.stderr, 'Unknown request: %s' % request_name
            sys.exit(1)
        try:
            return method(**params)
        except Exception, ex:
            self.display_error_and_exit(ex)

    def get_relative_filename(self, filename):
        return os.path.split(filename)[-1]

    def get_file_path(self, filename):
        relative_filename = self.get_relative_filename(filename)
        file_path = os.path.dirname(filename)
        if len(file_path) == 0:
            file_path = '.'
        return file_path

    def parse_block_device_args(self, block_device_maps_args):
        block_device_map = BlockDeviceMapping()
        for block_device_map_arg in block_device_maps_args:
            parts = block_device_map_arg.split('=')
            if len(parts) > 1:
                device_name = parts[0]
                block_dev_type = BlockDeviceType()
                block_dev_type.delete_on_termination = True
                value_parts = parts[1].split(':')
                if value_parts[0].startswith('snap'):
                    block_dev_type.snapshot_id = value_parts[0]
                else:
                    if value_parts[0].startswith('ephemeral'):
                        block_dev_type.ephemeral_name = value_parts[0]
                if len(value_parts) > 1:
                    try:
                        block_dev_type.size = int(value_parts[1])
                    except ValueError:
                        pass
                if len(value_parts) > 2:
                    if value_parts[2] == 'false':
                        block_dev_type.delete_on_termination = False
                block_device_map[device_name] = block_dev_type
        return block_device_map

    def get_endpoint_url(self, region_name):
        """
        Get the URL needed to reach a region with a given name.

        This currently only works with EC2.  In the future it may use other
        means to also work with Eucalyptus.
        """
        endpoint_template = 'https://ec2.%s.amazonaws.com/'
        endpoint_url      = endpoint_template % region_name
        endpoint_dnsname  = urlparse.urlparse(endpoint_url).hostname
        try:
            socket.getaddrinfo(endpoint_dnsname, None)
        except socket.gaierror:
            raise KeyError('Cannot resolve endpoint %s' % endpoint_dnsname)
        return endpoint_url
