#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
#    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.

"""
Keystone Identity Server - CLI Management Interface
"""

import sys
import logging
import optparse  # deprecated in 2.7, in favor of argparse

from keystone import version
import keystone.backends as db
from keystone.backends.sqlalchemy import migration
from keystone.common import config
from keystone.logic.types import fault
from keystone.manage import api


logger = logging.getLogger(__name__)


# CLI feature set
OBJECTS = ['user', 'tenant', 'role', 'service',
    'endpointTemplates', 'token', 'endpoint', 'credentials', 'database']
ACTIONS = ['add', 'list', 'disable', 'delete', 'grant', 'revoke',
    'sync', 'downgrade', 'upgrade', 'version_control', 'version',
    'goto']


# Messages
OBJECT_NOT_SPECIFIED = 'No object type specified for first argument'
ACTION_NOT_SPECIFIED = 'No action specified for second argument'
ID_NOT_SPECIFIED = 'No ID specified for third argument'
SUPPORTED_OBJECTS = "Supported objects: %s" % (", ".join(OBJECTS))
SUPPORTED_ACTIONS = "Supported actions: %s" % (", ".join(ACTIONS))
ACTION_NOT_SUPPORTED = 'Action not supported for %s'


class RaisingOptionParser(optparse.OptionParser):
    def error(self, msg):
        self.print_usage(sys.stderr)
        raise optparse.OptParseError(msg)


def parse_args(args=None):
    usage = """
    Usage: keystone-manage [options] type action [id [attributes]]
      type       : %s
      action     : %s
      id         : name or id
      attributes : depending on type...
        users    : password, tenant
        tokens   : user, tenant, expiration

      role list [tenant] will list roles granted on that tenant
      database [sync | downgrade | upgrade | version_control | version]

    options
      -c | --config-file : config file to use
      -d | --debug : debug mode

    Example: keystone-manage user add Admin P@ssw0rd
    """ % (", ".join(OBJECTS), ", ".join(ACTIONS))

    # Initialize a parser for our configuration paramaters
    parser = RaisingOptionParser(usage, version='%%prog %s'
        % version.version())
    config.add_common_options(parser)
    config.add_log_options(parser)

    # Parse command-line and load config
    (options, args) = config.parse_options(parser, args)
    _config_file, conf = config.load_paste_config('admin', options, args)

    config.setup_logging(options, conf)

    if not args or args[0] != 'database':
        db.configure_backends(conf.global_conf)

    return args


def get_options(args=None):
    # Initialize a parser for our configuration paramaters
    parser = RaisingOptionParser()
    config.add_common_options(parser)
    config.add_log_options(parser)

    # Parse command-line and load config
    (options, args) = config.parse_options(parser, list(args))

    _config_file, conf = config.load_paste_config('admin', options, args)
    conf.global_conf.update(conf.local_conf)

    return conf.global_conf


def process(*args):
    # Check arguments
    if len(args) == 0:
        raise optparse.OptParseError(OBJECT_NOT_SPECIFIED)
    else:
        object_type = args[0]
        if object_type not in OBJECTS:
            raise optparse.OptParseError(SUPPORTED_OBJECTS)

    if len(args) == 1:
        raise optparse.OptParseError(ACTION_NOT_SPECIFIED)
    else:
        action = args[1]
        if action not in ACTIONS:
            raise optparse.OptParseError(SUPPORTED_ACTIONS)

    if action not in ['list', 'sync', 'version_control', 'version']:
        if len(args) == 2:
            raise optparse.OptParseError(ID_NOT_SPECIFIED)
        else:
            object_id = args[2]

    # Helper functions
    def require_args(args, min, msg):
        """Ensure there are at least `min` arguments"""
        if len(args) < min:
            raise optparse.OptParseError(msg)

    optional_arg = (lambda args, x:
        len(args) > x and str(args[x]).strip() or None)

    if object_type == 'database':
        options = get_options(args)

    # Execute command
    if (object_type, action) == ('user', 'add'):
        require_args(args, 4, 'No password specified for fourth argument')
        if api.add_user(name=object_id, password=args[3],
                tenant=optional_arg(args, 4)):
            print "SUCCESS: User %s created." % object_id

    elif (object_type, action) == ('user', 'list'):
        print Table('Users', ['id', 'name', 'enabled', 'tenant'],
                    api.list_users())

    elif (object_type, action) == ('user', 'disable'):
        if api.disable_user(name=object_id):
            print "SUCCESS: User %s disabled." % object_id

    elif object_type == 'user':
        raise optparse.OptParseError(ACTION_NOT_SUPPORTED % ('users'))

    elif (object_type, action) == ('tenant', 'add'):
        if api.add_tenant(name=object_id):
            print "SUCCESS: Tenant %s created." % object_id

    elif (object_type, action) == ('tenant', 'list'):
        print Table('Tenants', ['id', 'name', 'enabled'], api.list_tenants())

    elif (object_type, action) == ('tenant', 'disable'):
        if api.disable_tenant(name=object_id):
            print "SUCCESS: Tenant %s disabled." % object_id

    elif object_type == 'tenant':
        raise optparse.OptParseError(ACTION_NOT_SUPPORTED % ('tenants'))

    elif (object_type, action) == ('role', 'add'):
        if api.add_role(name=object_id):
            print "SUCCESS: Role %s created successfully." % object_id

    elif (object_type, action) == ('role', 'list'):
        tenant = optional_arg(args, 2)
        if tenant:
            # print with users
            print Table('Role assignments for tenant %s' % tenant,
                        ['User', 'Role'],
                       api.list_roles(tenant=tenant))
        else:
            # print without tenants
            print Table('Roles', ['id', 'name', 'service_id', 'description'],
                api.list_roles())

    elif (object_type, action) == ('role', 'grant'):
        require_args(args, 4, "Missing arguments: role grant 'role' 'user' "
            "'tenant (optional)'")
        tenant = optional_arg(args, 4)
        if api.grant_role(object_id, args[3], tenant):
            print("SUCCESS: Granted %s the %s role on %s." %
                (args[3], object_id, tenant))

    elif object_type == 'role':
        raise optparse.OptParseError(ACTION_NOT_SUPPORTED % ('roles'))

    elif (object_type, action) == ('endpointTemplates', 'add'):
        require_args(args, 9, "Missing arguments: endpointTemplates add "
            "'region' 'service_name' 'publicURL' 'adminURL' 'internalURL' "
            "'enabled' 'global'")
        version_id = optional_arg(args, 9)
        version_list = optional_arg(args, 10)
        version_info = optional_arg(args, 11)
        if api.add_endpoint_template(region=args[2], service=args[3],
                public_url=args[4], admin_url=args[5], internal_url=args[6],
                enabled=args[7], is_global=args[8],
                version_id=version_id, version_list=version_list,
                version_info=version_info):
            print("SUCCESS: Created EndpointTemplates for %s pointing to %s." %
                (args[3], args[4]))

    elif (object_type, action) == ('endpointTemplates', 'list'):
        tenant = optional_arg(args, 2)
        if tenant:
            print Table('Endpoints for tenant %s' % tenant,
                        ['id', 'service', 'region', 'Public URL'],
                        api.list_tenant_endpoints(tenant))
        else:
            print Table('All EndpointTemplates', ['id', 'service', 'type',
                        'region', 'enabled', 'is_global', 'Public URL',
                        'Admin URL'],
                api.list_endpoint_templates())

    elif (object_type, action) == ('endpoint', 'add'):
        require_args(args, 4, "Missing arguments: endPoint add tenant "
            "endPointTemplate")
        if api.add_endpoint(tenant=args[2], endpoint_template=args[3]):
            print("SUCCESS: Endpoint %s added to tenant %s." %
                (args[3], args[2]))

    elif object_type == 'endpoint':
        raise optparse.OptParseError(ACTION_NOT_SUPPORTED % ('endpoints'))

    elif (object_type, action) == ('token', 'add'):
        require_args(args, 6, 'Creating a token requires a token id, user, '
            'tenant, and expiration')
        if api.add_token(token=object_id, user=args[3], tenant=args[4],
                expires=args[5]):
            print "SUCCESS: Token %s created." % (object_id,)

    elif (object_type, action) == ('token', 'list'):
        print Table('Tokens', ('token', 'user', 'expiration', 'tenant'),
            api.list_tokens())

    elif (object_type, action) == ('token', 'delete'):
        if api.delete_token(token=object_id):
            print 'SUCCESS: Token %s deleted.' % (object_id,)

    elif object_type == 'token':
        raise optparse.OptParseError(ACTION_NOT_SUPPORTED % ('tokens'))

    elif (object_type, action) == ('service', 'add'):
        require_args(args, 4, "Missing arguments: service add <name> " \
                     "[type] [desc] [owner_id]"
            "type")
        type = optional_arg(args, 3)
        desc = optional_arg(args, 4)
        owner_id = optional_arg(args, 5)

        if api.add_service(name=object_id, type=type, desc=desc,
                           owner_id=owner_id):
            print "SUCCESS: Service %s created successfully." % (object_id,)

    elif (object_type, action) == ('service', 'list'):
        print Table('Services',
                    ('id', 'name', 'type', 'owner_id', 'description'),
                    api.list_services())

    elif object_type == 'service':
        raise optparse.OptParseError(ACTION_NOT_SUPPORTED % ('services'))

    elif (object_type, action) == ('credentials', 'add'):
        require_args(args, 6, 'Creating a credentials requires a type, key, '
            'secret, and tenant_id (id is user_id)')
        if api.add_credentials(user=object_id, type=args[3], key=args[4],
                secrete=args[5], tenant=optional_arg(args, 6)):
            print "SUCCESS: Credentials %s created." % object_id

    elif object_type == 'credentials':
        raise optparse.OptParseError(ACTION_NOT_SUPPORTED % ('credentials'))

    elif (object_type, action) == ('database', 'sync'):
        require_args(args, 1, 'Syncing database requires a version #')
        backend_names = options.get('backends', None)
        if backend_names:
            if 'keystone.backends.sqlalchemy' in backend_names.split(','):
                do_db_sync(options['keystone.backends.sqlalchemy'],
                                 args)
            else:
                raise optparse.OptParseError(
                    'SQL alchemy backend not specified in config')

    elif (object_type, action) == ('database', 'upgrade'):
        require_args(args, 1, 'Upgrading database requires a version #')
        backend_names = options.get('backends', None)
        if backend_names:
            if 'keystone.backends.sqlalchemy' in backend_names.split(','):
                do_db_upgrade(options['keystone.backends.sqlalchemy'],
                                 args)
            else:
                raise optparse.OptParseError(
                    'SQL alchemy backend not specified in config')

    elif (object_type, action) == ('database', 'downgrade'):
        require_args(args, 1, 'Downgrading database requires a version #')
        backend_names = options.get('backends', None)
        if backend_names:
            if 'keystone.backends.sqlalchemy' in backend_names.split(','):
                do_db_downgrade(options['keystone.backends.sqlalchemy'],
                                 args)
            else:
                raise optparse.OptParseError(
                    'SQL alchemy backend not specified in config')

    elif (object_type, action) == ('database', 'version_control'):
        backend_names = options.get('backends', None)
        if backend_names:
            if 'keystone.backends.sqlalchemy' in backend_names.split(','):
                do_db_version_control(options['keystone.backends.sqlalchemy'])
            else:
                raise optparse.OptParseError(
                    'SQL alchemy backend not specified in config')

    elif (object_type, action) == ('database', 'version'):
        backend_names = options.get('backends', None)
        if backend_names:
            if 'keystone.backends.sqlalchemy' in backend_names.split(','):
                do_db_version(options['keystone.backends.sqlalchemy'])
            else:
                raise optparse.OptParseError(
                    'SQL alchemy backend not specified in config')

    elif (object_type, action) == ('database', 'goto'):
        require_args(args, 1, 'Jumping database versions requires a '
            'version #')
        backend_names = options.get('backends', None)
        if backend_names:
            if 'keystone.backends.sqlalchemy' in backend_names.split(','):
                do_db_goto_version(options['keystone.backends.sqlalchemy'],
                    version=args[2])
            else:
                raise optparse.OptParseError(
                    'SQL alchemy backend not specified in config')

    else:
        # Command recognized but not handled: should never reach this
        raise NotImplementedError()


#
#   Database Migration commands (from Glance-manage)
#
def do_db_version(options):
    """Print database's current migration level"""
    print migration.db_version(options)


def do_db_goto_version(options, version):
    """Override the database's current migration level"""
    if migration.db_goto_version(options, version):
        msg = ('Jumped to version=%s (without performing intermediate '
            'migrations)') % version
        print msg


def do_db_upgrade(options, args):
    """Upgrade the database's migration level"""
    try:
        db_version = args[2]
    except IndexError:
        db_version = None

    print "Upgrading database to version %s" % db_version
    migration.upgrade(options, version=db_version)


def do_db_downgrade(options, args):
    """Downgrade the database's migration level"""
    try:
        db_version = args[2]
    except IndexError:
        raise Exception("downgrade requires a version argument")

    migration.downgrade(options, version=db_version)


def do_db_version_control(options):
    """Place a database under migration control"""
    migration.version_control(options)
    print "Database now under version control"


def do_db_sync(options, args):
    """Place a database under migration control and upgrade"""
    try:
        db_version = args[2]
    except IndexError:
        db_version = None
    migration.db_sync(options, version=db_version)


class Table:
    """Prints data in table for console output

    Syntax print Table("This is the title",
            ["Header1","Header2","H3"],
            [[1,2,3],["Hi","everybody","How are you??"],[None,True,[1,2]]])

    """
    def __init__(self, title=None, headers=None, rows=None):
        self.title = title
        self.headers = headers if headers is not None else []
        self.rows = rows if rows is not None else []
        self.nrows = len(self.rows)
        self.fieldlen = []

        ncols = len(headers)

        for h in range(ncols):
            max = 0
            for row in rows:
                if len(str(row[h])) > max:
                    max = len(str(row[h]))
            self.fieldlen.append(max)

        for i in range(len(headers)):
            if len(str(headers[i])) > self.fieldlen[i]:
                self.fieldlen[i] = len(str(headers[i]))

        self.width = sum(self.fieldlen) + (ncols - 1) * 3 + 4

    def __str__(self):
        bar = "-" * self.width
        if self.title:
            title = "| " + self.title + " " * \
                    (self.width - 3 - (len(self.title))) + "|"
            out = [bar, title, bar]
        else:
            out = []
        header = ""
        for i in range(len(self.headers)):
            header += "| %s" % (str(self.headers[i])) + " " * \
                (self.fieldlen[i] - len(str(self.headers[i]))) + " "
        header += "|"
        out.append(header)
        out.append(bar)
        for i in self.rows:
            line = ""
            for j in range(len(i)):
                line += "| %s" % (str(i[j])) + " " * \
                (self.fieldlen[j] - len(str(i[j]))) + " "
            out.append(line + "|")

        out.append(bar)
        return "\r\n".join(out)


def main(args=None):
    try:
        process(*parse_args(args))
    except (optparse.OptParseError, fault.DatabaseMigrationError) as exc:
        print >> sys.stderr, exc
        sys.exit(2)
    except Exception as exc:
        try:
            info = exc.args[1]
        except IndexError:
            print "ERROR: %s" % (exc,)
            logger.error(str(exc))
        else:
            print "ERROR: %s: %s" % (exc.args[0], info)
            logger.error(exc.args[0], exc_info=info)
        raise exc


if __name__ == '__main__':
    try:
        main()
    except Exception as exc:
        sys.exit(1)
