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