#!/usr/bin/env python

# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
# Copyright 2016 Red Hat, Inc.
# 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.

# TODO(mandre)
# If possible get info from ironic for hosts prior to deployment

from __future__ import print_function

import json
import os
import sys

from heatclient import client as heat_client
from keystoneauth1.identity import generic as ks_id
from keystoneauth1 import session
import mistralclient.api.base
import mistralclient.api.client
from novaclient import client as nova_client
from oslo_config import cfg

opts = [
    cfg.StrOpt('host', help='List details about the specific host'),
    cfg.BoolOpt('list', help='List active hosts'),
    cfg.StrOpt('username', default=os.environ.get('OS_USERNAME')),
    cfg.StrOpt('password', default=os.environ.get('OS_PASSWORD')),
    cfg.StrOpt('auth-url', default=os.environ.get('OS_AUTH_URL')),
    cfg.StrOpt('auth-token', default=os.environ.get('OS_AUTH_TOKEN')),
    cfg.StrOpt('project-name', default=os.environ.get('OS_TENANT_NAME')),
    cfg.StrOpt('cacert', default=os.environ.get('OS_CACERT')),
    cfg.StrOpt('plan', default=os.environ.get('TRIPLEO_PLAN_NAME')),
]


def _parse_config():
    default_config = os.environ.get('TRIPLEO_INVENTORY_CONFIG')
    if default_config:
        default_config = [default_config]

    configs = cfg.ConfigOpts()
    configs.register_cli_opts(opts)
    configs(prog='tripleo-ansible-inventory',
            default_config_files=default_config)
    if configs.auth_url is None:
        print('ERROR: auth-url not defined and OS_AUTH_URL environment '
              'variable missing, unable to proceed.', file=sys.stderr)
        sys.exit(1)
    if '/v2.0' in configs.auth_url:
        configs.auth_url = configs.auth_url.replace('/v2.0', '/v3')
    if not configs.plan:
        configs.plan = 'overcloud'
    return configs


class TripleoInventory(object):
    def __init__(self, configs):
        self.configs = configs
        self._session = None
        self._ksclient = None
        self._hclient = None
        self._mclient = None
        self._nclient = None

    def fetch_stack_resources(self, resource_name):
        heatclient = self.hclient
        novaclient = self.nclient
        stack = self.configs.plan
        ret = []
        try:
            resource_id = heatclient.resources.get(stack, resource_name) \
                .physical_resource_id
            for resource in heatclient.resources.list(resource_id):
                node = heatclient.resources.get(resource_id,
                                                resource.resource_name)
                node_resource = node.attributes['nova_server_resource']
                nova_server = novaclient.servers.get(node_resource)
                if nova_server.status == 'ACTIVE':
                    ret.append(nova_server.networks['ctlplane'][0])
        except Exception:
            # Ignore non existent stacks or resources
            pass
        return ret

    def get_overcloud_output(self, output_name):
        try:
            stack = self.hclient.stacks.get(self.configs.plan)
            for output in stack.outputs:
                if output['output_key'] == output_name:
                    return output['output_value']
        except Exception:
            return None

    def get_overcloud_environment(self):
        try:
            environment = self.mclient.environments.get(self.configs.plan)
            return environment.variables
        except mistralclient.api.base.APIException:
            return {}

    def list(self):
        ret = {
            'undercloud': {
                'hosts': ['localhost'],
                'vars': {
                    'ansible_connection': 'local',
                },
            }
        }

        public_vip = self.get_overcloud_output('PublicVip')
        if public_vip:
            ret['undercloud']['vars']['overcloud_public_vip'] = public_vip
        keystone_url = self.get_overcloud_output('KeystoneURL')
        if public_vip:
            ret['undercloud']['vars']['overcloud_keystone_url'] = keystone_url
        overcloud_environment = self.get_overcloud_environment()
        passwords = overcloud_environment.get('passwords', {})
        admin_password = passwords.get('AdminPassword', '')
        if admin_password:
            ret['undercloud']['vars']['overcloud_admin_password'] = admin_password
        endpoint_map = self.get_overcloud_output('EndpointMap')
        if endpoint_map:
            horizon_endpoint = endpoint_map.get('HorizonPublic', {}).get('uri')
            if horizon_endpoint:
                ret['undercloud']['vars']['overcloud_horizon_url'] = horizon_endpoint

        controller_group = self.fetch_stack_resources('Controller')
        if controller_group:
            ret['controller'] = controller_group

        compute_group = self.fetch_stack_resources('Compute')
        if compute_group:
            ret['compute'] = compute_group

        if any([controller_group, compute_group]):
            ret['overcloud'] = {
                'children': list(set(ret.keys()) - set(['undercloud'])),
                'vars': {
                    # TODO(mandre) retrieve SSH user from heat
                    'ansible_ssh_user': 'heat-admin',
                }
            }

        print(json.dumps(ret))

    def host(self):
        # NOTE(mandre)
        # Dynamic inventory scripts must return empty json if they don't
        # provide detailed info for hosts:
        # http://docs.ansible.com/ansible/developing_inventory.html
        print(json.dumps({}))

    @property
    def session(self):
        if self._session is None:
            if self.configs.auth_token:
                auth = ks_id.Token(auth_url=self.configs.auth_url,
                                   token=self.configs.auth_token,
                                   project_name=self.configs.project_name,
                                   project_domain_id='default')
            else:
                auth = ks_id.Password(auth_url=self.configs.auth_url,
                                      username=self.configs.username,
                                      password=self.configs.password,
                                      project_name=self.configs.project_name,
                                      user_domain_id='default',
                                      project_domain_id='default')

            self._session = session.Session(auth=auth,
                                            verify=self.configs.cacert)
        return self._session

    @property
    def hclient(self):
        if self._hclient is None:
            try:
                self._hclient = heat_client.Client('1', session=self.session)
            except Exception as e:
                print("Error connecting to Heat: {}".format(e.message),
                      file=sys.stderr)
                sys.exit(1)
        return self._hclient

    @property
    def nclient(self):
        if self._nclient is None:
            try:
                self._nclient = nova_client.Client('2', session=self.session)
            except Exception as e:
                print("Error connecting to Nova: {}".format(e.message),
                      file=sys.stderr)
                sys.exit(1)
        return self._nclient

    @property
    def mclient(self):
        if self._mclient is None:
            try:
                endpoint = self.session.get_endpoint(service_type='workflowv2')
                self._mclient = mistralclient.api.client.client(
                    mistral_url=endpoint,
                    auth_token=self.session.get_token())
            except Exception as e:
                print("Error connecting to Mistral: {}".format(e.message),
                      file=sys.stderr)
                sys.exit(1)
        return self._mclient


def main():
    configs = _parse_config()
    inventory = TripleoInventory(configs)
    if configs.list:
        inventory.list()
    elif configs.host:
        inventory.host()
    sys.exit(0)

if __name__ == '__main__':
    main()
