mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-06-04 07:19:10 -07:00
verify_file was improperly always returning true if pyvimomi and requests libs were correct moved library checking to parse, avoid unneded errors unless the file is actually meant for this plugin
417 lines
17 KiB
Python
417 lines
17 KiB
Python
#
|
|
# Copyright: (c) 2018, Ansible Project
|
|
# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
|
|
#
|
|
# 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
|
|
|
|
return valid
|
|
|
|
def parse(self, inventory, loader, path, cache=True):
|
|
"""
|
|
Parses the inventory file
|
|
"""
|
|
|
|
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__))
|
|
|
|
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])
|