1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3'''
4VMware Inventory Script
5=======================
6
7Retrieve information about virtual machines from a vCenter server or
8standalone ESX host.  When `group_by=false` (in the INI file), host systems
9are also returned in addition to VMs.
10
11This script will attempt to read configuration from an INI file with the same
12base filename if present, or `vmware.ini` if not.  It is possible to create
13symlinks to the inventory script to support multiple configurations, e.g.:
14
15* `vmware.py` (this script)
16* `vmware.ini` (default configuration, will be read by `vmware.py`)
17* `vmware_test.py` (symlink to `vmware.py`)
18* `vmware_test.ini` (test configuration, will be read by `vmware_test.py`)
19* `vmware_other.py` (symlink to `vmware.py`, will read `vmware.ini` since no
20  `vmware_other.ini` exists)
21
22The path to an INI file may also be specified via the `VMWARE_INI` environment
23variable, in which case the filename matching rules above will not apply.
24
25Host and authentication parameters may be specified via the `VMWARE_HOST`,
26`VMWARE_USER` and `VMWARE_PASSWORD` environment variables; these options will
27take precedence over options present in the INI file.  An INI file is not
28required if these options are specified using environment variables.
29'''
30
31from __future__ import print_function
32
33import json
34import logging
35import optparse
36import os
37import ssl
38import sys
39import time
40
41from ansible.module_utils.common._collections_compat import MutableMapping
42from ansible.module_utils.six import integer_types, text_type, string_types
43from ansible.module_utils.six.moves import configparser
44
45# Disable logging message trigged by pSphere/suds.
46try:
47    from logging import NullHandler
48except ImportError:
49    from logging import Handler
50
51    class NullHandler(Handler):
52        def emit(self, record):
53            pass
54
55logging.getLogger('psphere').addHandler(NullHandler())
56logging.getLogger('suds').addHandler(NullHandler())
57
58from psphere.client import Client
59from psphere.errors import ObjectNotFoundError
60from psphere.managedobjects import HostSystem, VirtualMachine, ManagedObject, Network, ClusterComputeResource
61from suds.sudsobject import Object as SudsObject
62
63
64class VMwareInventory(object):
65
66    def __init__(self, guests_only=None):
67        self.config = configparser.SafeConfigParser()
68        if os.environ.get('VMWARE_INI', ''):
69            config_files = [os.environ['VMWARE_INI']]
70        else:
71            config_files = [os.path.abspath(sys.argv[0]).rstrip('.py') + '.ini', 'vmware.ini']
72        for config_file in config_files:
73            if os.path.exists(config_file):
74                self.config.read(config_file)
75                break
76
77        # Retrieve only guest VMs, or include host systems?
78        if guests_only is not None:
79            self.guests_only = guests_only
80        elif self.config.has_option('defaults', 'guests_only'):
81            self.guests_only = self.config.getboolean('defaults', 'guests_only')
82        else:
83            self.guests_only = True
84
85        # Read authentication information from VMware environment variables
86        # (if set), otherwise from INI file.
87        auth_host = os.environ.get('VMWARE_HOST')
88        if not auth_host and self.config.has_option('auth', 'host'):
89            auth_host = self.config.get('auth', 'host')
90        auth_user = os.environ.get('VMWARE_USER')
91        if not auth_user and self.config.has_option('auth', 'user'):
92            auth_user = self.config.get('auth', 'user')
93        auth_password = os.environ.get('VMWARE_PASSWORD')
94        if not auth_password and self.config.has_option('auth', 'password'):
95            auth_password = self.config.get('auth', 'password')
96        sslcheck = os.environ.get('VMWARE_SSLCHECK')
97        if not sslcheck and self.config.has_option('auth', 'sslcheck'):
98            sslcheck = self.config.get('auth', 'sslcheck')
99        if not sslcheck:
100            sslcheck = True
101        else:
102            if sslcheck.lower() in ['no', 'false']:
103                sslcheck = False
104            else:
105                sslcheck = True
106
107        # Limit the clusters being scanned
108        self.filter_clusters = os.environ.get('VMWARE_CLUSTERS')
109        if not self.filter_clusters and self.config.has_option('defaults', 'clusters'):
110            self.filter_clusters = self.config.get('defaults', 'clusters')
111        if self.filter_clusters:
112            self.filter_clusters = [x.strip() for x in self.filter_clusters.split(',') if x.strip()]
113
114        # Override certificate checks
115        if not sslcheck:
116            if hasattr(ssl, '_create_unverified_context'):
117                ssl._create_default_https_context = ssl._create_unverified_context
118
119        # Create the VMware client connection.
120        self.client = Client(auth_host, auth_user, auth_password)
121
122    def _put_cache(self, name, value):
123        '''
124        Saves the value to cache with the name given.
125        '''
126        if self.config.has_option('defaults', 'cache_dir'):
127            cache_dir = os.path.expanduser(self.config.get('defaults', 'cache_dir'))
128            if not os.path.exists(cache_dir):
129                os.makedirs(cache_dir)
130            cache_file = os.path.join(cache_dir, name)
131            with open(cache_file, 'w') as cache:
132                json.dump(value, cache)
133
134    def _get_cache(self, name, default=None):
135        '''
136        Retrieves the value from cache for the given name.
137        '''
138        if self.config.has_option('defaults', 'cache_dir'):
139            cache_dir = self.config.get('defaults', 'cache_dir')
140            cache_file = os.path.join(cache_dir, name)
141            if os.path.exists(cache_file):
142                if self.config.has_option('defaults', 'cache_max_age'):
143                    cache_max_age = self.config.getint('defaults', 'cache_max_age')
144                else:
145                    cache_max_age = 0
146                cache_stat = os.stat(cache_file)
147                if (cache_stat.st_mtime + cache_max_age) >= time.time():
148                    with open(cache_file) as cache:
149                        return json.load(cache)
150        return default
151
152    def _flatten_dict(self, d, parent_key='', sep='_'):
153        '''
154        Flatten nested dicts by combining keys with a separator.  Lists with
155        only string items are included as is; any other lists are discarded.
156        '''
157        items = []
158        for k, v in d.items():
159            if k.startswith('_'):
160                continue
161            new_key = parent_key + sep + k if parent_key else k
162            if isinstance(v, MutableMapping):
163                items.extend(self._flatten_dict(v, new_key, sep).items())
164            elif isinstance(v, (list, tuple)):
165                if all([isinstance(x, string_types) for x in v]):
166                    items.append((new_key, v))
167            else:
168                items.append((new_key, v))
169        return dict(items)
170
171    def _get_obj_info(self, obj, depth=99, seen=None):
172        '''
173        Recursively build a data structure for the given pSphere object (depth
174        only applies to ManagedObject instances).
175        '''
176        seen = seen or set()
177        if isinstance(obj, ManagedObject):
178            try:
179                obj_unicode = text_type(getattr(obj, 'name'))
180            except AttributeError:
181                obj_unicode = ()
182            if obj in seen:
183                return obj_unicode
184            seen.add(obj)
185            if depth <= 0:
186                return obj_unicode
187            d = {}
188            for attr in dir(obj):
189                if attr.startswith('_'):
190                    continue
191                try:
192                    val = getattr(obj, attr)
193                    obj_info = self._get_obj_info(val, depth - 1, seen)
194                    if obj_info != ():
195                        d[attr] = obj_info
196                except Exception as e:
197                    pass
198            return d
199        elif isinstance(obj, SudsObject):
200            d = {}
201            for key, val in iter(obj):
202                obj_info = self._get_obj_info(val, depth, seen)
203                if obj_info != ():
204                    d[key] = obj_info
205            return d
206        elif isinstance(obj, (list, tuple)):
207            l = []
208            for val in iter(obj):
209                obj_info = self._get_obj_info(val, depth, seen)
210                if obj_info != ():
211                    l.append(obj_info)
212            return l
213        elif isinstance(obj, (type(None), bool, float) + string_types + integer_types):
214            return obj
215        else:
216            return ()
217
218    def _get_host_info(self, host, prefix='vmware'):
219        '''
220        Return a flattened dict with info about the given host system.
221        '''
222        host_info = {
223            'name': host.name,
224        }
225        for attr in ('datastore', 'network', 'vm'):
226            try:
227                value = getattr(host, attr)
228                host_info['%ss' % attr] = self._get_obj_info(value, depth=0)
229            except AttributeError:
230                host_info['%ss' % attr] = []
231        for k, v in self._get_obj_info(host.summary, depth=0).items():
232            if isinstance(v, MutableMapping):
233                for k2, v2 in v.items():
234                    host_info[k2] = v2
235            elif k != 'host':
236                host_info[k] = v
237        try:
238            host_info['ipAddress'] = host.config.network.vnic[0].spec.ip.ipAddress
239        except Exception as e:
240            print(e, file=sys.stderr)
241        host_info = self._flatten_dict(host_info, prefix)
242        if ('%s_ipAddress' % prefix) in host_info:
243            host_info['ansible_ssh_host'] = host_info['%s_ipAddress' % prefix]
244        return host_info
245
246    def _get_vm_info(self, vm, prefix='vmware'):
247        '''
248        Return a flattened dict with info about the given virtual machine.
249        '''
250        vm_info = {
251            'name': vm.name,
252        }
253        for attr in ('datastore', 'network'):
254            try:
255                value = getattr(vm, attr)
256                vm_info['%ss' % attr] = self._get_obj_info(value, depth=0)
257            except AttributeError:
258                vm_info['%ss' % attr] = []
259        try:
260            vm_info['resourcePool'] = self._get_obj_info(vm.resourcePool, depth=0)
261        except AttributeError:
262            vm_info['resourcePool'] = ''
263        try:
264            vm_info['guestState'] = vm.guest.guestState
265        except AttributeError:
266            vm_info['guestState'] = ''
267        for k, v in self._get_obj_info(vm.summary, depth=0).items():
268            if isinstance(v, MutableMapping):
269                for k2, v2 in v.items():
270                    if k2 == 'host':
271                        k2 = 'hostSystem'
272                    vm_info[k2] = v2
273            elif k != 'vm':
274                vm_info[k] = v
275        vm_info = self._flatten_dict(vm_info, prefix)
276        if ('%s_ipAddress' % prefix) in vm_info:
277            vm_info['ansible_ssh_host'] = vm_info['%s_ipAddress' % prefix]
278        return vm_info
279
280    def _add_host(self, inv, parent_group, host_name):
281        '''
282        Add the host to the parent group in the given inventory.
283        '''
284        p_group = inv.setdefault(parent_group, [])
285        if isinstance(p_group, dict):
286            group_hosts = p_group.setdefault('hosts', [])
287        else:
288            group_hosts = p_group
289        if host_name not in group_hosts:
290            group_hosts.append(host_name)
291
292    def _add_child(self, inv, parent_group, child_group):
293        '''
294        Add a child group to a parent group in the given inventory.
295        '''
296        if parent_group != 'all':
297            p_group = inv.setdefault(parent_group, {})
298            if not isinstance(p_group, dict):
299                inv[parent_group] = {'hosts': p_group}
300                p_group = inv[parent_group]
301            group_children = p_group.setdefault('children', [])
302            if child_group not in group_children:
303                group_children.append(child_group)
304        inv.setdefault(child_group, [])
305
306    def get_inventory(self, meta_hostvars=True):
307        '''
308        Reads the inventory from cache or VMware API via pSphere.
309        '''
310        # Use different cache names for guests only vs. all hosts.
311        if self.guests_only:
312            cache_name = '__inventory_guests__'
313        else:
314            cache_name = '__inventory_all__'
315
316        inv = self._get_cache(cache_name, None)
317        if inv is not None:
318            return inv
319
320        inv = {'all': {'hosts': []}}
321        if meta_hostvars:
322            inv['_meta'] = {'hostvars': {}}
323
324        default_group = os.path.basename(sys.argv[0]).rstrip('.py')
325
326        if not self.guests_only:
327            if self.config.has_option('defaults', 'hw_group'):
328                hw_group = self.config.get('defaults', 'hw_group')
329            else:
330                hw_group = default_group + '_hw'
331
332        if self.config.has_option('defaults', 'vm_group'):
333            vm_group = self.config.get('defaults', 'vm_group')
334        else:
335            vm_group = default_group + '_vm'
336
337        if self.config.has_option('defaults', 'prefix_filter'):
338            prefix_filter = self.config.get('defaults', 'prefix_filter')
339        else:
340            prefix_filter = None
341
342        if self.filter_clusters:
343            # Loop through clusters and find hosts:
344            hosts = []
345            for cluster in ClusterComputeResource.all(self.client):
346                if cluster.name in self.filter_clusters:
347                    for host in cluster.host:
348                        hosts.append(host)
349        else:
350            # Get list of all physical hosts
351            hosts = HostSystem.all(self.client)
352
353        # Loop through physical hosts:
354        for host in hosts:
355
356            if not self.guests_only:
357                self._add_host(inv, 'all', host.name)
358                self._add_host(inv, hw_group, host.name)
359                host_info = self._get_host_info(host)
360                if meta_hostvars:
361                    inv['_meta']['hostvars'][host.name] = host_info
362                self._put_cache(host.name, host_info)
363
364            # Loop through all VMs on physical host.
365            for vm in host.vm:
366                if prefix_filter:
367                    if vm.name.startswith(prefix_filter):
368                        continue
369                self._add_host(inv, 'all', vm.name)
370                self._add_host(inv, vm_group, vm.name)
371                vm_info = self._get_vm_info(vm)
372                if meta_hostvars:
373                    inv['_meta']['hostvars'][vm.name] = vm_info
374                self._put_cache(vm.name, vm_info)
375
376                # Group by resource pool.
377                vm_resourcePool = vm_info.get('vmware_resourcePool', None)
378                if vm_resourcePool:
379                    self._add_child(inv, vm_group, 'resource_pools')
380                    self._add_child(inv, 'resource_pools', vm_resourcePool)
381                    self._add_host(inv, vm_resourcePool, vm.name)
382
383                # Group by datastore.
384                for vm_datastore in vm_info.get('vmware_datastores', []):
385                    self._add_child(inv, vm_group, 'datastores')
386                    self._add_child(inv, 'datastores', vm_datastore)
387                    self._add_host(inv, vm_datastore, vm.name)
388
389                # Group by network.
390                for vm_network in vm_info.get('vmware_networks', []):
391                    self._add_child(inv, vm_group, 'networks')
392                    self._add_child(inv, 'networks', vm_network)
393                    self._add_host(inv, vm_network, vm.name)
394
395                # Group by guest OS.
396                vm_guestId = vm_info.get('vmware_guestId', None)
397                if vm_guestId:
398                    self._add_child(inv, vm_group, 'guests')
399                    self._add_child(inv, 'guests', vm_guestId)
400                    self._add_host(inv, vm_guestId, vm.name)
401
402                # Group all VM templates.
403                vm_template = vm_info.get('vmware_template', False)
404                if vm_template:
405                    self._add_child(inv, vm_group, 'templates')
406                    self._add_host(inv, 'templates', vm.name)
407
408        self._put_cache(cache_name, inv)
409        return inv
410
411    def get_host(self, hostname):
412        '''
413        Read info about a specific host or VM from cache or VMware API.
414        '''
415        inv = self._get_cache(hostname, None)
416        if inv is not None:
417            return inv
418
419        if not self.guests_only:
420            try:
421                host = HostSystem.get(self.client, name=hostname)
422                inv = self._get_host_info(host)
423            except ObjectNotFoundError:
424                pass
425
426        if inv is None:
427            try:
428                vm = VirtualMachine.get(self.client, name=hostname)
429                inv = self._get_vm_info(vm)
430            except ObjectNotFoundError:
431                pass
432
433        if inv is not None:
434            self._put_cache(hostname, inv)
435        return inv or {}
436
437
438def main():
439    parser = optparse.OptionParser()
440    parser.add_option('--list', action='store_true', dest='list',
441                      default=False, help='Output inventory groups and hosts')
442    parser.add_option('--host', dest='host', default=None, metavar='HOST',
443                      help='Output variables only for the given hostname')
444    # Additional options for use when running the script standalone, but never
445    # used by Ansible.
446    parser.add_option('--pretty', action='store_true', dest='pretty',
447                      default=False, help='Output nicely-formatted JSON')
448    parser.add_option('--include-host-systems', action='store_true',
449                      dest='include_host_systems', default=False,
450                      help='Include host systems in addition to VMs')
451    parser.add_option('--no-meta-hostvars', action='store_false',
452                      dest='meta_hostvars', default=True,
453                      help='Exclude [\'_meta\'][\'hostvars\'] with --list')
454    options, args = parser.parse_args()
455
456    if options.include_host_systems:
457        vmware_inventory = VMwareInventory(guests_only=False)
458    else:
459        vmware_inventory = VMwareInventory()
460    if options.host is not None:
461        inventory = vmware_inventory.get_host(options.host)
462    else:
463        inventory = vmware_inventory.get_inventory(options.meta_hostvars)
464
465    json_kwargs = {}
466    if options.pretty:
467        json_kwargs.update({'indent': 4, 'sort_keys': True})
468    json.dump(inventory, sys.stdout, **json_kwargs)
469
470
471if __name__ == '__main__':
472    main()
473