1#!/usr/bin/env python
2# Copyright 2015 IIX Inc.
3#
4# This file is part of Ansible
5#
6# Ansible is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# Ansible is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
18
19"""
20ovirt external inventory script
21=================================
22
23Generates inventory that Ansible can understand by making API requests to
24oVirt via the ovirt-engine-sdk-python library.
25
26When run against a specific host, this script returns the following variables
27based on the data obtained from the ovirt_sdk Node object:
28 - ovirt_uuid
29 - ovirt_id
30 - ovirt_image
31 - ovirt_machine_type
32 - ovirt_ips
33 - ovirt_name
34 - ovirt_description
35 - ovirt_status
36 - ovirt_zone
37 - ovirt_tags
38 - ovirt_stats
39
40When run in --list mode, instances are grouped by the following categories:
41
42 - zone:
43   zone group name.
44 - instance tags:
45   An entry is created for each tag.  For example, if you have two instances
46   with a common tag called 'foo', they will both be grouped together under
47   the 'tag_foo' name.
48 - network name:
49   the name of the network is appended to 'network_' (e.g. the 'default'
50   network will result in a group named 'network_default')
51 - running status:
52   group name prefixed with 'status_' (e.g. status_up, status_down,..)
53
54Examples:
55  Execute uname on all instances in the us-central1-a zone
56  $ ansible -i ovirt.py us-central1-a -m shell -a "/bin/uname -a"
57
58  Use the ovirt inventory script to print out instance specific information
59  $ contrib/inventory/ovirt.py --host my_instance
60
61Author: Josha Inglis <jinglis@iix.net> based on the gce.py by Eric Johnson <erjohnso@google.com>
62Version: 0.0.1
63"""
64
65USER_AGENT_PRODUCT = "Ansible-ovirt_inventory_plugin"
66USER_AGENT_VERSION = "v1"
67
68import sys
69import os
70import argparse
71from collections import defaultdict
72from ansible.module_utils.six.moves import configparser as ConfigParser
73
74import json
75
76try:
77    # noinspection PyUnresolvedReferences
78    from ovirtsdk.api import API
79    # noinspection PyUnresolvedReferences
80    from ovirtsdk.xml import params
81except ImportError:
82    print("ovirt inventory script requires ovirt-engine-sdk-python")
83    sys.exit(1)
84
85
86class OVirtInventory(object):
87    def __init__(self):
88        # Read settings and parse CLI arguments
89        self.args = self.parse_cli_args()
90        self.driver = self.get_ovirt_driver()
91
92        # Just display data for specific host
93        if self.args.host:
94            print(self.json_format_dict(
95                self.node_to_dict(self.get_instance(self.args.host)),
96                pretty=self.args.pretty
97            ))
98            sys.exit(0)
99
100        # Otherwise, assume user wants all instances grouped
101        print(
102            self.json_format_dict(
103                data=self.group_instances(),
104                pretty=self.args.pretty
105            )
106        )
107        sys.exit(0)
108
109    @staticmethod
110    def get_ovirt_driver():
111        """
112        Determine the ovirt authorization settings and return a ovirt_sdk driver.
113
114        :rtype : ovirtsdk.api.API
115        """
116        kwargs = {}
117
118        ovirt_ini_default_path = os.path.join(
119            os.path.dirname(os.path.realpath(__file__)), "ovirt.ini")
120        ovirt_ini_path = os.environ.get('OVIRT_INI_PATH', ovirt_ini_default_path)
121
122        # Create a ConfigParser.
123        # This provides empty defaults to each key, so that environment
124        # variable configuration (as opposed to INI configuration) is able
125        # to work.
126        config = ConfigParser.SafeConfigParser(defaults={
127            'ovirt_url': '',
128            'ovirt_username': '',
129            'ovirt_password': '',
130            'ovirt_api_secrets': '',
131        })
132        if 'ovirt' not in config.sections():
133            config.add_section('ovirt')
134        config.read(ovirt_ini_path)
135
136        # Attempt to get ovirt params from a configuration file, if one
137        # exists.
138        secrets_path = config.get('ovirt', 'ovirt_api_secrets')
139        secrets_found = False
140        try:
141            # noinspection PyUnresolvedReferences,PyPackageRequirements
142            import secrets
143
144            kwargs = getattr(secrets, 'OVIRT_KEYWORD_PARAMS', {})
145            secrets_found = True
146        except ImportError:
147            pass
148
149        if not secrets_found and secrets_path:
150            if not secrets_path.endswith('secrets.py'):
151                err = "Must specify ovirt_sdk secrets file as /absolute/path/to/secrets.py"
152                print(err)
153                sys.exit(1)
154            sys.path.append(os.path.dirname(secrets_path))
155            try:
156                # noinspection PyUnresolvedReferences,PyPackageRequirements
157                import secrets
158
159                kwargs = getattr(secrets, 'OVIRT_KEYWORD_PARAMS', {})
160            except ImportError:
161                pass
162        if not secrets_found:
163            kwargs = {
164                'url': config.get('ovirt', 'ovirt_url'),
165                'username': config.get('ovirt', 'ovirt_username'),
166                'password': config.get('ovirt', 'ovirt_password'),
167            }
168
169        # If the appropriate environment variables are set, they override
170        # other configuration; process those into our args and kwargs.
171        kwargs['url'] = os.environ.get('OVIRT_URL', kwargs['url'])
172        kwargs['username'] = next(val for val in [os.environ.get('OVIRT_EMAIL'), os.environ.get('OVIRT_USERNAME'), kwargs['username']] if val is not None)
173        kwargs['password'] = next(val for val in [os.environ.get('OVIRT_PASS'), os.environ.get('OVIRT_PASSWORD'), kwargs['password']] if val is not None)
174
175        # Retrieve and return the ovirt driver.
176        return API(insecure=True, **kwargs)
177
178    @staticmethod
179    def parse_cli_args():
180        """
181        Command line argument processing
182
183        :rtype : argparse.Namespace
184        """
185
186        parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on ovirt')
187        parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)')
188        parser.add_argument('--host', action='store', help='Get all information about an instance')
189        parser.add_argument('--pretty', action='store_true', default=False, help='Pretty format (default: False)')
190        return parser.parse_args()
191
192    def node_to_dict(self, inst):
193        """
194        :type inst: params.VM
195        """
196        if inst is None:
197            return {}
198
199        inst.get_custom_properties()
200        ips = [ip.get_address() for ip in inst.get_guest_info().get_ips().get_ip()] \
201            if inst.get_guest_info() is not None else []
202        stats = {}
203        for stat in inst.get_statistics().list():
204            stats[stat.get_name()] = stat.get_values().get_value()[0].get_datum()
205
206        return {
207            'ovirt_uuid': inst.get_id(),
208            'ovirt_id': inst.get_id(),
209            'ovirt_image': inst.get_os().get_type(),
210            'ovirt_machine_type': self.get_machine_type(inst),
211            'ovirt_ips': ips,
212            'ovirt_name': inst.get_name(),
213            'ovirt_description': inst.get_description(),
214            'ovirt_status': inst.get_status().get_state(),
215            'ovirt_zone': inst.get_cluster().get_id(),
216            'ovirt_tags': self.get_tags(inst),
217            'ovirt_stats': stats,
218            # Hosts don't have a public name, so we add an IP
219            'ansible_ssh_host': ips[0] if len(ips) > 0 else None
220        }
221
222    @staticmethod
223    def get_tags(inst):
224        """
225        :type inst: params.VM
226        """
227        return [x.get_name() for x in inst.get_tags().list()]
228
229    def get_machine_type(self, inst):
230        inst_type = inst.get_instance_type()
231        if inst_type:
232            return self.driver.instancetypes.get(id=inst_type.id).name
233
234    # noinspection PyBroadException,PyUnusedLocal
235    def get_instance(self, instance_name):
236        """Gets details about a specific instance """
237        try:
238            return self.driver.vms.get(name=instance_name)
239        except Exception as e:
240            return None
241
242    def group_instances(self):
243        """Group all instances"""
244        groups = defaultdict(list)
245        meta = {"hostvars": {}}
246
247        for node in self.driver.vms.list():
248            assert isinstance(node, params.VM)
249            name = node.get_name()
250
251            meta["hostvars"][name] = self.node_to_dict(node)
252
253            zone = node.get_cluster().get_name()
254            groups[zone].append(name)
255
256            tags = self.get_tags(node)
257            for t in tags:
258                tag = 'tag_%s' % t
259                groups[tag].append(name)
260
261            nets = [x.get_name() for x in node.get_nics().list()]
262            for net in nets:
263                net = 'network_%s' % net
264                groups[net].append(name)
265
266            status = node.get_status().get_state()
267            stat = 'status_%s' % status.lower()
268            if stat in groups:
269                groups[stat].append(name)
270            else:
271                groups[stat] = [name]
272
273        groups["_meta"] = meta
274
275        return groups
276
277    @staticmethod
278    def json_format_dict(data, pretty=False):
279        """ Converts a dict to a JSON object and dumps it as a formatted
280        string """
281
282        if pretty:
283            return json.dumps(data, sort_keys=True, indent=2)
284        else:
285            return json.dumps(data)
286
287
288# Run the script
289OVirtInventory()
290