#!/usr/bin/env python
# Copyright 2015 IIX Inc.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

"""
ovirt external inventory script
=================================

Generates inventory that Ansible can understand by making API requests to
oVirt via the ovirt-engine-sdk-python library.

When run against a specific host, this script returns the following variables
based on the data obtained from the ovirt_sdk Node object:
 - ovirt_uuid
 - ovirt_id
 - ovirt_image
 - ovirt_machine_type
 - ovirt_ips
 - ovirt_name
 - ovirt_description
 - ovirt_status
 - ovirt_zone
 - ovirt_tags
 - ovirt_stats

When run in --list mode, instances are grouped by the following categories:

 - zone:
   zone group name.
 - instance tags:
   An entry is created for each tag.  For example, if you have two instances
   with a common tag called 'foo', they will both be grouped together under
   the 'tag_foo' name.
 - network name:
   the name of the network is appended to 'network_' (e.g. the 'default'
   network will result in a group named 'network_default')
 - running status:
   group name prefixed with 'status_' (e.g. status_up, status_down,..)

Examples:
  Execute uname on all instances in the us-central1-a zone
  $ ansible -i ovirt.py us-central1-a -m shell -a "/bin/uname -a"

  Use the ovirt inventory script to print out instance specific information
  $ contrib/inventory/ovirt.py --host my_instance

Author: Josha Inglis <jinglis@iix.net> based on the gce.py by Eric Johnson <erjohnso@google.com>
Version: 0.0.1
"""

USER_AGENT_PRODUCT = "Ansible-ovirt_inventory_plugin"
USER_AGENT_VERSION = "v1"

import sys
import os
import argparse
import ConfigParser
from collections import defaultdict

import json

try:
    # noinspection PyUnresolvedReferences
    from ovirtsdk.api import API
    # noinspection PyUnresolvedReferences
    from ovirtsdk.xml import params
except ImportError:
    print("ovirt inventory script requires ovirt-engine-sdk-python")
    sys.exit(1)


class OVirtInventory(object):
    def __init__(self):
        # Read settings and parse CLI arguments
        self.args = self.parse_cli_args()
        self.driver = self.get_ovirt_driver()

        # Just display data for specific host
        if self.args.host:
            print(self.json_format_dict(
                self.node_to_dict(self.get_instance(self.args.host)),
                pretty=self.args.pretty
            ))
            sys.exit(0)

        # Otherwise, assume user wants all instances grouped
        print(
            self.json_format_dict(
                data=self.group_instances(),
                pretty=self.args.pretty
            )
        )
        sys.exit(0)

    @staticmethod
    def get_ovirt_driver():
        """
        Determine the ovirt authorization settings and return a ovirt_sdk driver.

        :rtype : ovirtsdk.api.API
        """
        kwargs = {}

        ovirt_ini_default_path = os.path.join(
            os.path.dirname(os.path.realpath(__file__)), "ovirt.ini")
        ovirt_ini_path = os.environ.get('OVIRT_INI_PATH', ovirt_ini_default_path)

        # Create a ConfigParser.
        # This provides empty defaults to each key, so that environment
        # variable configuration (as opposed to INI configuration) is able
        # to work.
        config = ConfigParser.SafeConfigParser(defaults={
            'ovirt_url': '',
            'ovirt_username': '',
            'ovirt_password': '',
            'ovirt_api_secrets': '',
        })
        if 'ovirt' not in config.sections():
            config.add_section('ovirt')
        config.read(ovirt_ini_path)

        # Attempt to get ovirt params from a configuration file, if one
        # exists.
        secrets_path = config.get('ovirt', 'ovirt_api_secrets')
        secrets_found = False
        try:
            # noinspection PyUnresolvedReferences,PyPackageRequirements
            import secrets

            kwargs = getattr(secrets, 'OVIRT_KEYWORD_PARAMS', {})
            secrets_found = True
        except ImportError:
            pass

        if not secrets_found and secrets_path:
            if not secrets_path.endswith('secrets.py'):
                err = "Must specify ovirt_sdk secrets file as /absolute/path/to/secrets.py"
                print(err)
                sys.exit(1)
            sys.path.append(os.path.dirname(secrets_path))
            try:
                # noinspection PyUnresolvedReferences,PyPackageRequirements
                import secrets

                kwargs = getattr(secrets, 'OVIRT_KEYWORD_PARAMS', {})
            except ImportError:
                pass
        if not secrets_found:
            kwargs = {
                'url': config.get('ovirt', 'ovirt_url'),
                'username': config.get('ovirt', 'ovirt_username'),
                'password': config.get('ovirt', 'ovirt_password'),
            }

        # If the appropriate environment variables are set, they override
        # other configuration; process those into our args and kwargs.
        kwargs['url'] = os.environ.get('OVIRT_URL', kwargs['url'])
        kwargs['username'] = next(val for val in [os.environ.get('OVIRT_EMAIL'), os.environ.get('OVIRT_USERNAME'), kwargs['username']] if val is not None)
        kwargs['password'] = next(val for val in [os.environ.get('OVIRT_PASS'), os.environ.get('OVIRT_PASSWORD'), kwargs['password']] if val is not None)

        # Retrieve and return the ovirt driver.
        return API(insecure=True, **kwargs)

    @staticmethod
    def parse_cli_args():
        """
        Command line argument processing

        :rtype : argparse.Namespace
        """

        parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on ovirt')
        parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)')
        parser.add_argument('--host', action='store', help='Get all information about an instance')
        parser.add_argument('--pretty', action='store_true', default=False, help='Pretty format (default: False)')
        return parser.parse_args()

    def node_to_dict(self, inst):
        """
        :type inst: params.VM
        """
        if inst is None:
            return {}

        inst.get_custom_properties()
        ips = [ip.get_address() for ip in inst.get_guest_info().get_ips().get_ip()] \
            if inst.get_guest_info() is not None else []
        stats = {}
        for stat in inst.get_statistics().list():
            stats[stat.get_name()] = stat.get_values().get_value()[0].get_datum()

        return {
            'ovirt_uuid': inst.get_id(),
            'ovirt_id': inst.get_id(),
            'ovirt_image': inst.get_os().get_type(),
            'ovirt_machine_type': self.get_machine_type(inst),
            'ovirt_ips': ips,
            'ovirt_name': inst.get_name(),
            'ovirt_description': inst.get_description(),
            'ovirt_status': inst.get_status().get_state(),
            'ovirt_zone': inst.get_cluster().get_id(),
            'ovirt_tags': self.get_tags(inst),
            'ovirt_stats': stats,
            # Hosts don't have a public name, so we add an IP
            'ansible_ssh_host': ips[0] if len(ips) > 0 else None
        }

    @staticmethod
    def get_tags(inst):
        """
        :type inst: params.VM
        """
        return [x.get_name() for x in inst.get_tags().list()]

    def get_machine_type(self, inst):
        inst_type = inst.get_instance_type()
        if inst_type:
            return self.driver.instancetypes.get(id=inst_type.id).name

    # noinspection PyBroadException,PyUnusedLocal
    def get_instance(self, instance_name):
        """Gets details about a specific instance """
        try:
            return self.driver.vms.get(name=instance_name)
        except Exception as e:
            return None

    def group_instances(self):
        """Group all instances"""
        groups = defaultdict(list)
        meta = {"hostvars": {}}

        for node in self.driver.vms.list():
            assert isinstance(node, params.VM)
            name = node.get_name()

            meta["hostvars"][name] = self.node_to_dict(node)

            zone = node.get_cluster().get_name()
            groups[zone].append(name)

            tags = self.get_tags(node)
            for t in tags:
                tag = 'tag_%s' % t
                groups[tag].append(name)

            nets = [x.get_name() for x in node.get_nics().list()]
            for net in nets:
                net = 'network_%s' % net
                groups[net].append(name)

            status = node.get_status().get_state()
            stat = 'status_%s' % status.lower()
            if stat in groups:
                groups[stat].append(name)
            else:
                groups[stat] = [name]

        groups["_meta"] = meta

        return groups

    @staticmethod
    def json_format_dict(data, pretty=False):
        """ Converts a dict to a JSON object and dumps it as a formatted
        string """

        if pretty:
            return json.dumps(data, sort_keys=True, indent=2)
        else:
            return json.dumps(data)


# Run the script
OVirtInventory()