1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2016 Red Hat, Inc.
5#
6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7
8from __future__ import (absolute_import, division, print_function)
9__metaclass__ = type
10
11
12"""
13oVirt dynamic inventory script
14=================================
15
16Generates dynamic inventory file for oVirt.
17
18Script will return following attributes for each virtual machine:
19 - id
20 - name
21 - host
22 - cluster
23 - status
24 - description
25 - fqdn
26 - os_type
27 - template
28 - tags
29 - statistics
30 - devices
31
32When run in --list mode, virtual machines are grouped by the following categories:
33 - cluster
34 - tag
35 - status
36
37 Note: If there is some virtual machine which has has more tags it will be in both tag
38       records.
39
40Examples:
41  # Execute update of system on webserver virtual machine:
42
43    $ ansible -i contrib/inventory/ovirt4.py webserver -m yum -a "name=* state=latest"
44
45  # Get webserver virtual machine information:
46
47    $ contrib/inventory/ovirt4.py --host webserver
48
49Author: Ondra Machacek (@machacekondra)
50"""
51
52import argparse
53import os
54import sys
55
56from collections import defaultdict
57
58from ansible.module_utils.six.moves import configparser
59from ansible.module_utils.six import PY2
60
61import json
62
63try:
64    import ovirtsdk4 as sdk
65    import ovirtsdk4.types as otypes
66except ImportError:
67    print('oVirt inventory script requires ovirt-engine-sdk-python >= 4.0.0')
68    sys.exit(1)
69
70
71def parse_args():
72    """
73    Create command line parser for oVirt dynamic inventory script.
74    """
75    parser = argparse.ArgumentParser(
76        description='Ansible dynamic inventory script for oVirt.',
77    )
78    parser.add_argument(
79        '--list',
80        action='store_true',
81        default=True,
82        help='Get data of all virtual machines (default: True).',
83    )
84    parser.add_argument(
85        '--host',
86        help='Get data of virtual machines running on specified host.',
87    )
88    parser.add_argument(
89        '--pretty',
90        action='store_true',
91        default=False,
92        help='Pretty format (default: False).',
93    )
94    return parser.parse_args()
95
96
97def create_connection():
98    """
99    Create a connection to oVirt engine API.
100    """
101    # Get the path of the configuration file, by default use
102    # 'ovirt.ini' file in script directory:
103    default_path = os.path.join(
104        os.path.dirname(os.path.realpath(__file__)),
105        'ovirt.ini',
106    )
107    config_path = os.environ.get('OVIRT_INI_PATH', default_path)
108
109    # Create parser and add ovirt section if it doesn't exist:
110    if PY2:
111        config = configparser.SafeConfigParser(
112            defaults={
113                'ovirt_url': os.environ.get('OVIRT_URL'),
114                'ovirt_username': os.environ.get('OVIRT_USERNAME'),
115                'ovirt_password': os.environ.get('OVIRT_PASSWORD'),
116                'ovirt_ca_file': os.environ.get('OVIRT_CAFILE', ''),
117            }, allow_no_value=True
118        )
119    else:
120        config = configparser.ConfigParser(
121            defaults={
122                'ovirt_url': os.environ.get('OVIRT_URL'),
123                'ovirt_username': os.environ.get('OVIRT_USERNAME'),
124                'ovirt_password': os.environ.get('OVIRT_PASSWORD'),
125                'ovirt_ca_file': os.environ.get('OVIRT_CAFILE', ''),
126            }, allow_no_value=True
127        )
128    if not config.has_section('ovirt'):
129        config.add_section('ovirt')
130    config.read(config_path)
131
132    # Create a connection with options defined in ini file:
133    return sdk.Connection(
134        url=config.get('ovirt', 'ovirt_url'),
135        username=config.get('ovirt', 'ovirt_username'),
136        password=config.get('ovirt', 'ovirt_password', raw=True),
137        ca_file=config.get('ovirt', 'ovirt_ca_file') or None,
138        insecure=not config.get('ovirt', 'ovirt_ca_file'),
139    )
140
141
142def get_dict_of_struct(connection, vm):
143    """
144    Transform SDK Vm Struct type to Python dictionary.
145    """
146    if vm is None:
147        return dict()
148
149    vms_service = connection.system_service().vms_service()
150    clusters_service = connection.system_service().clusters_service()
151    vm_service = vms_service.vm_service(vm.id)
152    devices = vm_service.reported_devices_service().list()
153    tags = vm_service.tags_service().list()
154    stats = vm_service.statistics_service().list()
155    labels = vm_service.affinity_labels_service().list()
156    groups = clusters_service.cluster_service(
157        vm.cluster.id
158    ).affinity_groups_service().list()
159
160    return {
161        'id': vm.id,
162        'name': vm.name,
163        'host': connection.follow_link(vm.host).name if vm.host else None,
164        'cluster': connection.follow_link(vm.cluster).name,
165        'status': str(vm.status),
166        'description': vm.description,
167        'fqdn': vm.fqdn,
168        'os_type': vm.os.type,
169        'template': connection.follow_link(vm.template).name,
170        'tags': [tag.name for tag in tags],
171        'affinity_labels': [label.name for label in labels],
172        'affinity_groups': [
173            group.name for group in groups
174            if vm.name in [vm.name for vm in connection.follow_link(group.vms)]
175        ],
176        'statistics': dict(
177            (stat.name, stat.values[0].datum) for stat in stats if stat.values
178        ),
179        'devices': dict(
180            (device.name, [ip.address for ip in device.ips]) for device in devices if device.ips
181        ),
182        'ansible_host': next((device.ips[0].address for device in devices if device.ips), None)
183    }
184
185
186def get_data(connection, vm_name=None):
187    """
188    Obtain data of `vm_name` if specified, otherwise obtain data of all vms.
189    """
190    vms_service = connection.system_service().vms_service()
191    clusters_service = connection.system_service().clusters_service()
192
193    if vm_name:
194        vm = vms_service.list(search='name=%s' % vm_name) or [None]
195        data = get_dict_of_struct(
196            connection=connection,
197            vm=vm[0],
198        )
199    else:
200        vms = dict()
201        data = defaultdict(list)
202        for vm in vms_service.list():
203            name = vm.name
204            vm_service = vms_service.vm_service(vm.id)
205            cluster_service = clusters_service.cluster_service(vm.cluster.id)
206
207            # Add vm to vms dict:
208            vms[name] = get_dict_of_struct(connection, vm)
209
210            # Add vm to cluster group:
211            cluster_name = connection.follow_link(vm.cluster).name
212            data['cluster_%s' % cluster_name].append(name)
213
214            # Add vm to tag group:
215            tags_service = vm_service.tags_service()
216            for tag in tags_service.list():
217                data['tag_%s' % tag.name].append(name)
218
219            # Add vm to status group:
220            data['status_%s' % vm.status].append(name)
221
222            # Add vm to affinity group:
223            for group in cluster_service.affinity_groups_service().list():
224                if vm.name in [
225                    v.name for v in connection.follow_link(group.vms)
226                ]:
227                    data['affinity_group_%s' % group.name].append(vm.name)
228
229            # Add vm to affinity label group:
230            affinity_labels_service = vm_service.affinity_labels_service()
231            for label in affinity_labels_service.list():
232                data['affinity_label_%s' % label.name].append(name)
233
234        data["_meta"] = {
235            'hostvars': vms,
236        }
237
238    return data
239
240
241def main():
242    args = parse_args()
243    connection = create_connection()
244
245    print(
246        json.dumps(
247            obj=get_data(
248                connection=connection,
249                vm_name=args.host,
250            ),
251            sort_keys=args.pretty,
252            indent=args.pretty * 2,
253        )
254    )
255
256
257if __name__ == '__main__':
258    main()
259