# # Copyright: (c) 2018, Ansible Project # Copyright: (c) 2018, Abhijeet Kasurde # # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' name: vmware_vm_inventory plugin_type: inventory short_description: VMware Guest inventory source version_added: "2.6" description: - Get virtual machines as inventory hosts from VMware environment. - Uses any file which ends with vmware.yml or vmware.yaml as a YAML configuration file. - The inventory_hostname is always the 'Name' and UUID of the virtual machine. UUID is added as VMware allows virtual machines with the same name. extends_documentation_fragment: - inventory_cache requirements: - "Python >= 2.7" - "PyVmomi" - "requests >= 2.3" - "vSphere Automation SDK - For tag feature" - "vCloud Suite SDK - For tag feature" options: hostname: description: Name of vCenter or ESXi server. required: True env: - name: VMWARE_SERVER username: description: Name of vSphere admin user. required: True env: - name: VMWARE_USERNAME password: description: Password of vSphere admin user. required: True env: - name: VMWARE_PASSWORD port: description: Port number used to connect to vCenter or ESXi Server. default: 443 env: - name: VMWARE_PORT validate_certs: description: - Allows connection when SSL certificates are not valid. Set to C(false) when certificates are not trusted. default: True type: boolean with_tags: description: - Include tags and associated virtual machines. - Requires 'vSphere Automation SDK' and 'vCloud Suite SDK' libraries to be installed on the given controller machine. - Please refer following URLs for installation steps - 'https://code.vmware.com/web/sdk/65/vsphere-automation-python' - 'https://code.vmware.com/web/sdk/60/vcloudsuite-python' default: False type: boolean ''' EXAMPLES = ''' # Sample configuration file for VMware Guest dynamic inventory plugin: vmware_vm_inventory strict: False hostname: 10.65.223.31 username: administrator@vsphere.local password: Esxi@123$% validate_certs: False with_tags: True ''' import ssl import atexit from ansible.errors import AnsibleError, AnsibleParserError try: # requests is required for exception handling of the ConnectionError import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False try: from pyVim import connect from pyVmomi import vim, vmodl HAS_PYVMOMI = True except ImportError: HAS_PYVMOMI = False try: from vmware.vapi.lib.connect import get_requests_connector from vmware.vapi.security.session import create_session_security_context from vmware.vapi.security.user_password import create_user_password_security_context from com.vmware.cis_client import Session from com.vmware.vapi.std_client import DynamicID from com.vmware.cis.tagging_client import Tag, TagAssociation HAS_VCLOUD = True except ImportError: HAS_VCLOUD = False try: from vmware.vapi.stdlib.client.factories import StubConfigurationFactory HAS_VSPHERE = True except ImportError: HAS_VSPHERE = False from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable class InventoryModule(BaseInventoryPlugin, Cacheable): NAME = 'vmware_vm_inventory' def _set_credentials(self): """ Set credentials """ self.hostname = self.get_option('hostname') self.username = self.get_option('username') self.password = self.get_option('password') self.port = self.get_option('port') self.with_tags = self.get_option('with_tags') self.validate_certs = self.get_option('validate_certs') if not HAS_VSPHERE and self.with_tags: raise AnsibleError("Unable to find 'vSphere Automation SDK' Python library which is required." " Please refer this URL for installation steps" " - https://code.vmware.com/web/sdk/65/vsphere-automation-python") if not HAS_VCLOUD and self.with_tags: raise AnsibleError("Unable to find 'vCloud Suite SDK' Python library which is required." " Please refer this URL for installation steps" " - https://code.vmware.com/web/sdk/60/vcloudsuite-python") if not all([self.hostname, self.username, self.password]): raise AnsibleError("Missing one of the following : hostname, username, password. Please read " "the documentation for more information.") def _login_vapi(self): """ Login to vCenter API using REST call Returns: connection object """ session = requests.Session() session.verify = self.validate_certs if not self.validate_certs: # Disable warning shown at stdout requests.packages.urllib3.disable_warnings() vcenter_url = "https://%s/api" % self.hostname # Get request connector connector = get_requests_connector(session=session, url=vcenter_url) # Create standard Configuration stub_config = StubConfigurationFactory.new_std_configuration(connector) # Use username and password in the security context to authenticate security_context = create_user_password_security_context(self.username, self.password) # Login stub_config.connector.set_security_context(security_context) # Create the stub for the session service and login by creating a session. session_svc = Session(stub_config) session_id = session_svc.create() # After successful authentication, store the session identifier in the security # context of the stub and use that for all subsequent remote requests session_security_context = create_session_security_context(session_id) stub_config.connector.set_security_context(session_security_context) if stub_config is None: raise AnsibleError("Failed to login to %s using %s" % (self.hostname, self.username)) return stub_config def _login(self): """ Login to vCenter or ESXi server Returns: connection object """ if self.validate_certs and not hasattr(ssl, 'SSLContext'): raise AnsibleError('pyVim does not support changing verification mode with python < 2.7.9. Either update ' 'python or set validate_certs to false in configuration YAML file.') ssl_context = None if not self.validate_certs and hasattr(ssl, 'SSLContext'): ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) ssl_context.verify_mode = ssl.CERT_NONE service_instance = None try: service_instance = connect.SmartConnect(host=self.hostname, user=self.username, pwd=self.password, sslContext=ssl_context, port=self.port) except vim.fault.InvalidLogin as e: raise AnsibleParserError("Unable to log on to vCenter or ESXi API at %s:%s as %s: %s" % (self.hostname, self.port, self.username, e.msg)) except vim.fault.NoPermission as e: raise AnsibleParserError("User %s does not have required permission" " to log on to vCenter or ESXi API at %s:%s : %s" % (self.username, self.hostname, self.port, e.msg)) except (requests.ConnectionError, ssl.SSLError) as e: raise AnsibleParserError("Unable to connect to vCenter or ESXi API at %s on TCP/%s: %s" % (self.hostname, self.port, e)) except vmodl.fault.InvalidRequest as e: # Request is malformed raise AnsibleParserError("Failed to get a response from server %s:%s as " "request is malformed: %s" % (self.hostname, self.port, e.msg)) except Exception as e: raise AnsibleParserError("Unknown error while connecting to vCenter or ESXi API at %s:%s : %s" % (self.hostname, self.port, e)) if service_instance is None: raise AnsibleParserError("Unknown error while connecting to vCenter or ESXi API at %s:%s" % (self.hostname, self.port)) atexit.register(connect.Disconnect, service_instance) return service_instance.RetrieveContent() def verify_file(self, path): """ Verify plugin configuration file and mark this plugin active Args: path: Path of configuration YAML file Returns: True if everything is correct, else False """ valid = False if super(InventoryModule, self).verify_file(path): if path.endswith(('vmware.yaml', 'vmware.yml')): valid = True if not HAS_REQUESTS: raise AnsibleParserError('Please install "requests" Python module as this is required' ' for VMware Guest dynamic inventory plugin.') elif not HAS_PYVMOMI: raise AnsibleParserError('Please install "PyVmomi" Python module as this is required' ' for VMware Guest dynamic inventory plugin.') if HAS_REQUESTS: # Pyvmomi 5.5 and onwards requires requests 2.3 # https://github.com/vmware/pyvmomi/blob/master/requirements.txt required_version = (2, 3) requests_version = requests.__version__.split(".")[:2] try: requests_major_minor = tuple(map(int, requests_version)) except ValueError: raise AnsibleParserError("Failed to parse 'requests' library version.") if requests_major_minor < required_version: raise AnsibleParserError("'requests' library version should" " be >= %s, found: %s." % (".".join([str(w) for w in required_version]), requests.__version__)) valid = True return valid def parse(self, inventory, loader, path, cache=True): """ Parses the inventory file """ super(InventoryModule, self).parse(inventory, loader, path, cache=cache) cache_key = self.get_cache_key(path) config_data = self._read_config_data(path) source_data = None if cache: cache = self.get_option('cache') update_cache = False if cache: try: source_data = self.cache.get(cache_key) except KeyError: update_cache = True # set _options from config data self._consume_options(config_data) self._set_credentials() self.content = self._login() if self.with_tags: self.rest_content = self._login_vapi() using_current_cache = cache and not update_cache cacheable_results = self._populate_from_source(source_data, using_current_cache) if update_cache: self.cache.set(cache_key, cacheable_results) def _populate_from_cache(self, source_data): """ Populate inventory from cache """ hostvars = source_data.pop('_meta', {}).get('hostvars', {}) for group in source_data: if group == 'all': continue else: self.inventory.add_group(group) self.inventory.add_child('all', group) if not source_data: for host in hostvars: self.inventory.add_host(host) def _populate_from_source(self, source_data, using_current_cache): """ Populate inventory data from direct source """ if using_current_cache: self._populate_from_cache(source_data) return source_data cacheable_results = {} hostvars = {} objects = self._get_managed_objects_properties(vim_type=vim.VirtualMachine, properties=['name']) if self.with_tags: tag_svc = Tag(self.rest_content) tag_association = TagAssociation(self.rest_content) tags_info = dict() tags = tag_svc.list() for tag in tags: tag_obj = tag_svc.get(tag) tags_info[tag_obj.id] = tag_obj.name if tag_obj.name not in cacheable_results: cacheable_results[tag_obj.name] = {'hosts': []} self.inventory.add_group(tag_obj.name) for temp_vm_object in objects: for temp_vm_object_property in temp_vm_object.propSet: # VMware does not provide a way to uniquely identify VM by its name # i.e. there can be two virtual machines with same name # Appending "_" and VMware UUID to make it unique current_host = temp_vm_object_property.val + "_" + temp_vm_object.obj.config.uuid if current_host not in hostvars: hostvars[current_host] = {} self.inventory.add_host(current_host) # Only gather facts related to tag if vCloud and vSphere is installed. if HAS_VCLOUD and HAS_VSPHERE and self.with_tags: # Add virtual machine to appropriate tag group vm_mo_id = temp_vm_object.obj._GetMoId() vm_dynamic_id = DynamicID(type='VirtualMachine', id=vm_mo_id) attached_tags = tag_association.list_attached_tags(vm_dynamic_id) for tag_id in attached_tags: self.inventory.add_child(tags_info[tag_id], current_host) cacheable_results[tags_info[tag_id]]['hosts'].append(current_host) # Based on power state of virtual machine vm_power = temp_vm_object.obj.summary.runtime.powerState if vm_power not in cacheable_results: cacheable_results[vm_power] = [] self.inventory.add_group(vm_power) cacheable_results[vm_power].append(current_host) self.inventory.add_child(vm_power, current_host) # Based on guest id vm_guest_id = temp_vm_object.obj.config.guestId if vm_guest_id and vm_guest_id not in cacheable_results: cacheable_results[vm_guest_id] = [] self.inventory.add_group(vm_guest_id) cacheable_results[vm_guest_id].append(current_host) self.inventory.add_child(vm_guest_id, current_host) return cacheable_results def _get_managed_objects_properties(self, vim_type, properties=None): """ Look up a Managed Object Reference in vCenter / ESXi Environment :param vim_type: Type of vim object e.g, for datacenter - vim.Datacenter :param properties: List of properties related to vim object e.g. Name :return: local content object """ # Get Root Folder root_folder = self.content.rootFolder if properties is None: properties = ['name'] # Create Container View with default root folder mor = self.content.viewManager.CreateContainerView(root_folder, [vim_type], True) # Create Traversal spec traversal_spec = vmodl.query.PropertyCollector.TraversalSpec( name="traversal_spec", path='view', skip=False, type=vim.view.ContainerView ) # Create Property Spec property_spec = vmodl.query.PropertyCollector.PropertySpec( type=vim_type, # Type of object to retrieved all=False, pathSet=properties ) # Create Object Spec object_spec = vmodl.query.PropertyCollector.ObjectSpec( obj=mor, skip=True, selectSet=[traversal_spec] ) # Create Filter Spec filter_spec = vmodl.query.PropertyCollector.FilterSpec( objectSet=[object_spec], propSet=[property_spec], reportMissingObjectsInResults=False ) return self.content.propertyCollector.RetrieveContents([filter_spec])