#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
#    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.

"""
This is the administration program for heat. It is simply a command-line
interface for adding, modifying, and retrieving information about the stacks
belonging to a user.  It is a convenience application that talks to the heat
API server.
"""

import gettext
import optparse
import os
import os.path
import sys
import time
import logging

import httplib
from urlparse import urlparse
# If ../heat/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
                                   os.pardir,
                                   os.pardir))
if os.path.exists(os.path.join(possible_topdir, 'heat', '__init__.py')):
    sys.path.insert(0, possible_topdir)

scriptname = os.path.basename(sys.argv[0])

gettext.install('heat', unicode=1)

if scriptname == 'heat-boto':
    from heat.cfn_client import boto_client as heat_client
else:
    from heat.cfn_client import client as heat_client
from heat.version import version_info as version
from heat.common import config
from heat.common import exception
from heat.cfn_client import utils
from keystoneclient.v2_0 import client


def get_swift_template(options):
    '''
    Retrieve a template from the swift object store, using
    the provided URL.  We request a keystone token to authenticate
    '''
    template_body = None
    if options.auth_strategy == 'keystone':
        # we use the keystone credentials to get a token
        # to pass in the request header
        keystone = client.Client(username=options.username,
                                 password=options.password,
                                 tenant_name=options.tenant,
                                 auth_url=options.auth_url)
        logging.info("Getting template from swift URL: %s" %
                     options.template_object)
        url = urlparse(options.template_object)
        if url.scheme == 'https':
            conn = httplib.HTTPSConnection(url.netloc)
        else:
            conn = httplib.HTTPConnection(url.netloc)
        headers = {'X-Auth-Token': keystone.auth_token}
        conn.request("GET", url.path, headers=headers)
        r1 = conn.getresponse()
        logging.info('status %d' % r1.status)
        if r1.status == 200:
            template_body = r1.read()
            conn.close()
    else:
        logging.error("template-object option requires keystone")

    return template_body


def get_template_param(options):
    '''
    Helper function to extract the template in whatever
    format has been specified by the cli options
    '''
    param = {}
    if options.template_file:
        param['TemplateBody'] = open(options.template_file).read()
    elif options.template_url:
        param['TemplateUrl'] = options.template_url
    elif options.template_object:
        template_body = get_swift_template(options)
        if template_body:
            param['TemplateBody'] = template_body
        else:
            logging.error("Error reading swift template")

    return param


@utils.catch_error('validate')
def template_validate(options, arguments):
    '''
    Validate a template.  This command parses a template and verifies that
    it is in the correct format.

    Usage: heat-cfn validate \\
        [--template-file=<template file>|--template-url=<template URL>] \\
        [options]

    --template-file: Specify a local template file.
    --template-url:  Specify a URL pointing to a stack description template.
    '''
    parameters = {}
    templ_param = get_template_param(options)
    if templ_param:
        parameters.update(templ_param)
    else:
        logging.error('Please specify a template file or url')
        return utils.FAILURE

    c = get_client(options)
    parameters.update(c.format_parameters(options))
    result = c.validate_template(**parameters)
    print c.format_template(result)


@utils.catch_error('estimatetemplatecost')
def estimate_template_cost(options, arguments):
    parameters = {}
    try:
        parameters['StackName'] = arguments.pop(0)
    except IndexError:
        logging.error("Please specify the stack name you wish to estimate")
        logging.error("as the first argument")
        return utils.FAILURE

    templ_param = get_template_param(options)
    if templ_param:
        parameters.update(templ_param)
    else:
        logging.error('Please specify a template file or url')
        return utils.FAILURE

    c = get_client(options)
    parameters.update(c.format_parameters(options))
    result = c.estimate_template_cost(**parameters)
    print result


@utils.catch_error('gettemplate')
def get_template(options, arguments):
    '''
    Gets an existing stack template.
    '''
    parameters = {}
    try:
        parameters['StackName'] = arguments.pop(0)
    except IndexError:
        logging.error("Please specify the stack you wish to get")
        logging.error("as the first argument")
        return utils.FAILURE

    c = get_client(options)
    result = c.get_template(**parameters)
    print result


@utils.catch_error('create')
def stack_create(options, arguments):
    '''
    Create a new stack from a template.

    Usage: heat-cfn create <stack name> \\
        [--template-file=<template file>|--template-url=<template URL>] \\
        [options]

    Stack Name: The user specified name of the stack you wish to create.

    --template-file: Specify a local template file containing a valid
                     stack description template.
    --template-url:  Specify a URL pointing to a valid stack description
                     template.
    '''

    parameters = {}
    try:
        parameters['StackName'] = arguments.pop(0)
    except IndexError:
        logging.error("Please specify the stack name you wish to create")
        logging.error("as the first argument")
        return utils.FAILURE

    parameters['TimeoutInMinutes'] = options.timeout

    if options.enable_rollback:
        parameters['DisableRollback'] = 'False'

    templ_param = get_template_param(options)
    if templ_param:
        parameters.update(templ_param)
    else:
        logging.error('Please specify a template file or url')
        return utils.FAILURE

    c = get_client(options)
    parameters.update(c.format_parameters(options))
    result = c.create_stack(**parameters)
    print result


@utils.catch_error('update')
def stack_update(options, arguments):
    '''
    Update an existing stack.

    Usage: heat-cfn update <stack name> \\
        [--template-file=<template file>|--template-url=<template URL>] \\
        [options]

    Stack Name: The name of the stack you wish to modify.

    --template-file: Specify a local template file containing a valid
                     stack description template.
    --template-url:  Specify a URL pointing to a valid stack description
                     template.

    Options:
        --parameters: A list of key/value pairs separated by ';'s used
                      to specify allowed values in the template file.
    '''
    parameters = {}
    try:
        parameters['StackName'] = arguments.pop(0)
    except IndexError:
        logging.error("Please specify the stack name you wish to update")
        logging.error("as the first argument")
        return utils.FAILURE

    templ_param = get_template_param(options)
    if templ_param:
        parameters.update(templ_param)
    else:
        logging.error('Please specify a template file or url')
        return utils.FAILURE

    c = get_client(options)
    parameters.update(c.format_parameters(options))
    result = c.update_stack(**parameters)
    print result


@utils.catch_error('delete')
def stack_delete(options, arguments):
    '''
    Delete an existing stack.  This shuts down all VMs associated with
    the stack and (perhaps wrongly) also removes all events associated
    with the given stack.

    Usage: heat-cfn delete <stack name>
    '''
    parameters = {}
    try:
        parameters['StackName'] = arguments.pop(0)
    except IndexError:
        logging.error("Please specify the stack name you wish to delete")
        logging.error("as the first argument")
        return utils.FAILURE

    c = get_client(options)
    result = c.delete_stack(**parameters)
    print result


@utils.catch_error('describe')
def stack_describe(options, arguments):
    '''
    Describes an existing stack.

    Usage: heat-cfn describe <stack name>
    '''
    parameters = {}
    try:
        parameters['StackName'] = arguments.pop(0)
    except IndexError:
        logging.info("No stack name passed, getting results for ALL stacks")

    c = get_client(options)
    result = c.describe_stacks(**parameters)
    print c.format_stack(result)


@utils.catch_error('event-list')
def stack_events_list(options, arguments):
    '''
    List events associated with the given stack.

    Usage: heat-cfn event-list <stack name>
    '''
    parameters = {}
    try:
        parameters['StackName'] = arguments.pop(0)
    except IndexError:
        pass

    c = get_client(options)
    result = c.list_stack_events(**parameters)
    print c.format_stack_event(result)


@utils.catch_error('resource')
def stack_resource_show(options, arguments):
    '''
    Display details of the specified resource.
    '''
    c = get_client(options)
    try:
        stack_name, resource_name = arguments
    except ValueError:
        print 'Enter stack name and logical resource id'
        return

    parameters = {
        'StackName': stack_name,
        'LogicalResourceId': resource_name,
    }
    result = c.describe_stack_resource(**parameters)
    print c.format_stack_resource_detail(result)


@utils.catch_error('resource-list')
def stack_resources_list(options, arguments):
    '''
    Display summary of all resources in the specified stack.
    '''
    c = get_client(options)
    try:
        stack_name = arguments.pop(0)
    except IndexError:
        print 'Enter stack name'
        return

    parameters = {
        'StackName': stack_name,
    }
    result = c.list_stack_resources(**parameters)
    print c.format_stack_resource_summary(result)


@utils.catch_error('resource-list-details')
def stack_resources_list_details(options, arguments):
    '''
    Display details of resources in the specified stack.

    - If stack name is specified, all associated resources are returned
    - If physical resource ID is specified, all associated resources of the
    stack the resource belongs to are returned
    - You must specify stack name *or* physical resource ID
    - You may optionally specify a Logical resource ID to filter the result
    '''
    usage = ('''Usage:
%s resource-list-details stack_name [logical_resource_id]
%s resource-list-details physical_resource_id [logical_resource_id]''' %
             (scriptname, scriptname))

    try:
        name_or_pid = arguments.pop(0)
    except IndexError:
        logging.error("Must pass a stack_name or physical_resource_id")
        print usage
        return

    logical_resource_id = arguments.pop(0) if arguments else None
    parameters = {
        'NameOrPid': name_or_pid,
        'LogicalResourceId': logical_resource_id, }

    c = get_client(options)
    result = c.describe_stack_resources(**parameters)

    if result:
        print c.format_stack_resource(result)
    else:
        logging.error("Invalid stack_name, physical_resource_id " +
                      "or logical_resource_id")
        print usage


@utils.catch_error('list')
def stack_list(options, arguments):
    '''
    List all running stacks.

    Usage: heat-cfn list
    '''
    c = get_client(options)
    result = c.list_stacks()
    print c.format_stack_summary(result)


def get_client(options):
    """
    Returns a new client object to a heat server
    specified by the --host and --port options
    supplied to the CLI
    Note options.host is ignored for heat-boto, host must be
    set in your boto config via the cfn_region_endpoint option
    """
    return heat_client.get_client(host=options.host,
                                  port=options.port,
                                  username=options.username,
                                  password=options.password,
                                  tenant=options.tenant,
                                  auth_url=options.auth_url,
                                  auth_strategy=options.auth_strategy,
                                  auth_token=options.auth_token,
                                  region=options.region,
                                  insecure=options.insecure)


def create_options(parser):
    """
    Sets up the CLI and config-file options that may be
    parsed and program commands.

    :param parser: The option parser
    """
    parser.add_option('-v', '--verbose', default=False, action="store_true",
                      help="Print more verbose output")
    parser.add_option('-d', '--debug', default=False, action="store_true",
                      help="Print more verbose output")
    parser.add_option('-y', '--yes', default=False, action="store_true",
                      help="Don't prompt for user input; assume the answer to "
                           "every question is 'yes'.")
    parser.add_option('-H', '--host', metavar="ADDRESS", default=None,
                      help="Address of heat API host. "
                           "Default: %default")
    parser.add_option('-p', '--port', dest="port", metavar="PORT",
                      type=int, default=config.DEFAULT_PORT,
                      help="Port the heat API host listens on. "
                           "Default: %default")
    parser.add_option('-U', '--url', metavar="URL", default=None,
                      help="URL of heat service. This option can be used "
                           "to specify the hostname, port and protocol "
                           "(http/https) of the heat server, for example "
                           "-U https://localhost:" + str(config.DEFAULT_PORT) +
                           "/v1 Default: No<F3>ne")
    parser.add_option('-k', '--insecure', dest="insecure",
                      default=False, action="store_true",
                      help="Explicitly allow heat to perform \"insecure\" "
                      "SSL (https) requests. The server's certificate will "
                      "not be verified against any certificate authorities. "
                      "This option should be used with caution.")
    parser.add_option('-A', '--auth_token', dest="auth_token",
                      metavar="TOKEN", default=None,
                      help="Authentication token to use to identify the "
                           "client to the heat server")
    parser.add_option('-I', '--username', dest="username",
                      metavar="USER", default=None,
                      help="User name used to acquire an authentication "
                           "token, defaults to env[OS_USERNAME]")
    parser.add_option('-K', '--password', dest="password",
                      metavar="PASSWORD", default=None,
                      help="Password used to acquire an authentication "
                           "token, defaults to env[OS_PASSWORD]")
    parser.add_option('-T', '--tenant', dest="tenant",
                      metavar="TENANT", default=None,
                      help="Tenant name used for Keystone authentication, "
                           "defaults to env[OS_TENANT_NAME]")
    parser.add_option('-R', '--region', dest="region",
                      metavar="REGION", default=None,
                      help="Region name. When using keystone authentication "
                      "version 2.0 or later this identifies the region "
                      "name to use when selecting the service endpoint. A "
                      "region name must be provided if more than one "
                      "region endpoint is available")
    parser.add_option('-N', '--auth_url', dest="auth_url",
                      metavar="AUTH_URL", default=None,
                      help="Authentication URL, "
                           "defaults to env[OS_AUTH_URL]")
    parser.add_option('-S', '--auth_strategy', dest="auth_strategy",
                      metavar="STRATEGY", default=None,
                      help="Authentication strategy (keystone or noauth)")

    parser.add_option('-o', '--template-object',
                      metavar="template_object", default=None,
                      help="URL to retrieve template object (e.g from swift)")
    parser.add_option('-u', '--template-url', metavar="template_url",
                      default=None, help="URL of template. Default: None")
    parser.add_option('-f', '--template-file', metavar="template_file",
                      default=None, help="Path to the template. Default: None")
    parser.add_option('-t', '--timeout', metavar="timeout",
                      default='60',
                      help='Stack creation timeout in minutes. Default: 60')

    parser.add_option('-P', '--parameters', metavar="parameters", default=None,
                      help="Parameter values used to create the stack.")

    parser.add_option('-r', '--enable-rollback', dest="enable_rollback",
                      default=False, action="store_true",
                      help="Enable rollback on failure")


def credentials_from_env():
    return dict(username=os.getenv('OS_USERNAME'),
                password=os.getenv('OS_PASSWORD'),
                tenant=os.getenv('OS_TENANT_NAME'),
                auth_url=os.getenv('OS_AUTH_URL'),
                auth_strategy=os.getenv('OS_AUTH_STRATEGY'))


def parse_options(parser, cli_args):
    """
    Returns the parsed CLI options, command to run and its arguments, merged
    with any same-named options found in a configuration file

    :param parser: The option parser
    """
    if not cli_args:
        cli_args.append('-h')  # Show options in usage output...

    (options, args) = parser.parse_args(cli_args)
    env_opts = credentials_from_env()
    for option, env_val in env_opts.items():
        if not getattr(options, option):
            setattr(options, option, env_val)

    if scriptname == 'heat-boto':
        if options.host is not None:
            logging.error("Use boto.cfg or ~/.boto cfn_region_endpoint")
            raise ValueError("--host option not supported by heat-boto")
        if options.url is not None:
            logging.error("Use boto.cfg or ~/.boto cfn_region_endpoint")
            raise ValueError("--url option not supported by heat-boto")

    if not (options.username and options.password) and not options.auth_token:
        logging.error("Must specify credentials, " +
                      "either username/password or token")
        logging.error("See %s --help for options" % scriptname)
        sys.exit(1)

    if options.url is not None:
        u = urlparse(options.url)
        options.port = u.port
        options.host = u.hostname

    if not options.auth_strategy:
        options.auth_strategy = 'noauth'

    options.use_ssl = (options.url is not None and u.scheme == 'https')

    # HACK(sirp): Make the parser available to the print_help method
    # print_help is a command, so it only accepts (options, args); we could
    # one-off have it take (parser, options, args), however, for now, I think
    # this little hack will suffice
    options.__parser = parser

    if not args:
        parser.print_usage()
        sys.exit(0)

    command_name = args.pop(0)
    command = lookup_command(parser, command_name)

    if options.debug:
        logging.basicConfig(format='%(levelname)s:%(message)s',
                            level=logging.DEBUG)
        logging.debug("Debug level logging enabled")
    elif options.verbose:
        logging.basicConfig(format='%(levelname)s:%(message)s',
                            level=logging.INFO)
    else:
        logging.basicConfig(format='%(levelname)s:%(message)s',
                            level=logging.WARNING)

    return (options, command, args)


def print_help(options, args):
    """
    Print help specific to a command
    """
    parser = options.__parser

    if not args:
        parser.print_usage()

    subst = {'prog': os.path.basename(sys.argv[0])}
    docs = [lookup_command(parser, cmd).__doc__ % subst for cmd in args]
    print '\n\n'.join(docs)


def lookup_command(parser, command_name):
    base_commands = {'help': print_help}

    stack_commands = {'create': stack_create,
                      'update': stack_update,
                      'delete': stack_delete,
                      'list': stack_list,
                      'events_list': stack_events_list,  # DEPRECATED
                      'event-list': stack_events_list,
                      'resource': stack_resource_show,
                      'resource-list': stack_resources_list,
                      'resource-list-details': stack_resources_list_details,
                      'validate': template_validate,
                      'gettemplate': get_template,
                      'estimate-template-cost': estimate_template_cost,
                      'describe': stack_describe}

    commands = {}
    for command_set in (base_commands, stack_commands):
        commands.update(command_set)

    try:
        command = commands[command_name]
    except KeyError:
        parser.print_usage()
        sys.exit("Unknown command: %s" % command_name)

    return command


def main():
    '''
    '''
    usage = """
%prog <command> [options] [args]

Commands:

    help <command>  Output help for one of the commands below

    create          Create the stack

    delete          Delete the stack

    describe        Describe the stack

    update          Update the stack

    list            List the user's stacks

    gettemplate     Get the template

    estimate-template-cost     Returns the estimated monthly cost of a template

    validate        Validate a template

    event-list      List events for a stack

    resource        Describe the resource

    resource-list   Show list of resources belonging to a stack

    resource-list-details    Detailed view of resources belonging to a stack

"""

    version_string = version.version_string()
    oparser = optparse.OptionParser(version=version_string,
                                    usage=usage.strip())
    create_options(oparser)
    try:
        (opts, cmd, args) = parse_options(oparser, sys.argv[1:])
    except ValueError as ex:
        logging.error("Error parsing options : %s" % str(ex))
        sys.exit(1)

    try:
        start_time = time.time()
        result = cmd(opts, args)
        end_time = time.time()
        logging.debug("Completed in %-0.4f sec." % (end_time - start_time))
        sys.exit(result)
    except (RuntimeError,
            NotImplementedError,
            exception.ClientConfigurationError), ex:
        oparser.print_usage()
        logging.error("ERROR: %s" % ex)
        sys.exit(1)


if __name__ == '__main__':
    main()
