Skip to content

Commit

Permalink
vmware_vm_inventory support api access behind proxy (#818)
Browse files Browse the repository at this point in the history
vmware_vm_inventory support api access behind proxy

SUMMARY

add new parameters proxy_host, proxy_port and ssl_proxy_path in order to allow api access behind proxy.

#775
ISSUE TYPE


Bugfix Pull Request

COMPONENT NAME

vmware_vm_inventory
vmware_host_inventory
ADDITIONAL INFORMATION



---
plugin: community.vmware.vmware_vm_inventory
proxy_host: 192.168.123.5
proxy_port: 3128

Reviewed-by: Mario Lenz <[email protected]>
Reviewed-by: Abhijeet Kasurde <None>
Reviewed-by: sky-joker <[email protected]>
  • Loading branch information
abikouo authored Jun 28, 2021
1 parent 6643fc3 commit c859996
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minor_changes:
- vmware_vm_inventory - support api access via proxy (https://github.com/ansible-collections/community.vmware/pull/817).
- vmware_host_inventory - support api access via proxy (https://github.com/ansible-collections/community.vmware/pull/817).
23 changes: 21 additions & 2 deletions plugins/inventory/vmware_host_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,24 @@
- Also, transforms property name to snake case.
type: bool
default: False
proxy_host:
description:
- Address of a proxy that will receive all HTTPS requests and relay them.
- The format is a hostname or a IP.
- This feature depends on a version of pyvmomi>=v6.7.1.2018.12.
type: str
required: False
version_added: '1.12.0'
env:
- name: VMWARE_PROXY_HOST
proxy_port:
description:
- Port of the HTTP proxy that will receive all HTTPS requests and relay them.
type: int
required: False
version_added: '1.12.0'
env:
- name: VMWARE_PROXY_PORT
"""

EXAMPLES = r"""
Expand Down Expand Up @@ -174,10 +192,10 @@
from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
from ansible.module_utils.six import text_type
from ansible_collections.community.vmware.plugins.module_utils.inventory import (
BaseVMwareInventory,
to_nested_dict,
to_flatten_dict,
)
from ansible_collections.community.vmware.plugins.inventory.vmware_vm_inventory import BaseVMwareInventory
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode

Expand Down Expand Up @@ -236,7 +254,8 @@ def parse(self, inventory, loader, path, cache=True):
port=self.get_option("port"),
with_tags=self.get_option("with_tags"),
validate_certs=self.get_option("validate_certs"),
display=self.display,
http_proxy_host=self.get_option("proxy_host"),
http_proxy_port=self.get_option("proxy_port")
)

self.pyv.do_login()
Expand Down
69 changes: 29 additions & 40 deletions plugins/inventory/vmware_vm_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,24 @@
- Also, transforms property name to snake case.
type: bool
default: False
proxy_host:
description:
- Address of a proxy that will receive all HTTPS requests and relay them.
- The format is a hostname or a IP.
- This feature depends on a version of pyvmomi>=v6.7.1.2018.12.
type: str
required: False
version_added: '1.12.0'
env:
- name: VMWARE_PROXY_HOST
proxy_port:
description:
- Port of the HTTP proxy that will receive all HTTPS requests and relay them.
type: int
required: False
version_added: '1.12.0'
env:
- name: VMWARE_PROXY_PORT
'''

EXAMPLES = r'''
Expand Down Expand Up @@ -323,8 +341,6 @@
- 'guest.ipStack'
'''

import ssl
import atexit
import base64
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.module_utils._text import to_text, to_native
Expand All @@ -343,7 +359,6 @@
HAS_REQUESTS = False

try:
from pyVim import connect
from pyVmomi import vim, vmodl
from pyVmomi.VmomiSupport import DataObject
from pyVmomi import Iso8601
Expand All @@ -361,10 +376,11 @@

from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
from ansible_collections.community.vmware.plugins.module_utils.vmware import connect_to_api


class BaseVMwareInventory:
def __init__(self, hostname, username, password, port, validate_certs, with_tags):
def __init__(self, hostname, username, password, port, validate_certs, with_tags, http_proxy_host, http_proxy_port):
self.hostname = hostname
self.username = username
self.password = password
Expand All @@ -373,6 +389,8 @@ def __init__(self, hostname, username, password, port, validate_certs, with_tags
self.validate_certs = validate_certs
self.content = None
self.rest_content = None
self.proxy_host = http_proxy_host
self.proxy_port = http_proxy_port

def do_login(self):
"""
Expand Down Expand Up @@ -421,40 +439,10 @@ def _login(self):
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, service_instance.RetrieveContent()
return connect_to_api(module=None, disconnect_atexit=True, return_si=True,
hostname=self.hostname, username=self.username, password=self.password,
port=self.port, validate_certs=self.validate_certs, httpProxyHost=self.proxy_host,
httpProxyPort=self.proxy_port)

def check_requirements(self):
""" Check all requirements for this inventory are satisfied"""
Expand Down Expand Up @@ -738,9 +726,10 @@ def parse(self, inventory, loader, path, cache=True):
password=password,
port=self.get_option('port'),
with_tags=self.get_option('with_tags'),
validate_certs=self.get_option('validate_certs')
validate_certs=self.get_option('validate_certs'),
http_proxy_host=self.get_option('proxy_host'),
http_proxy_port=self.get_option('proxy_port')
)

self.pyv.do_login()

if cache:
Expand Down
77 changes: 48 additions & 29 deletions plugins/module_utils/vmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ def __init__(self, *args, **kwargs):
super(TaskError, self).__init__(*args, **kwargs)


class ApiAccessError(Exception):
def __init__(self, *args, **kwargs):
super(ApiAccessError, self).__init__(*args, **kwargs)


def check_answer_question_status(vm):
"""Check whether locked a virtual machine.
Expand Down Expand Up @@ -668,31 +673,47 @@ def vmware_argument_spec():
)


def connect_to_api(module, disconnect_atexit=True, return_si=False, hostname=None, username=None, password=None, port=None, validate_certs=None):
hostname = hostname if hostname else module.params['hostname']
username = username if username else module.params['username']
password = password if password else module.params['password']
port = port if port else module.params.get('port', 443)
validate_certs = validate_certs if validate_certs else module.params['validate_certs']
def connect_to_api(module, disconnect_atexit=True, return_si=False, hostname=None, username=None, password=None, port=None, validate_certs=None,
httpProxyHost=None, httpProxyPort=None):
if module:
if not hostname:
hostname = module.params['hostname']
if not username:
username = module.params['username']
if not password:
password = module.params['password']
if not httpProxyHost:
httpProxyHost = module.params.get('proxy_host')
if not httpProxyPort:
httpProxyPort = module.params.get('proxy_port')
if not port:
port = module.params.get('port', 443)
if not validate_certs:
validate_certs = module.params['validate_certs']

def _raise_or_fail(msg):
if module is not None:
module.fail_json(msg=msg)
raise ApiAccessError(msg)

if not hostname:
module.fail_json(msg="Hostname parameter is missing."
" Please specify this parameter in task or"
" export environment variable like 'export VMWARE_HOST=ESXI_HOSTNAME'")
_raise_or_fail(msg="Hostname parameter is missing."
" Please specify this parameter in task or"
" export environment variable like 'export VMWARE_HOST=ESXI_HOSTNAME'")

if not username:
module.fail_json(msg="Username parameter is missing."
" Please specify this parameter in task or"
" export environment variable like 'export VMWARE_USER=ESXI_USERNAME'")
_raise_or_fail(msg="Username parameter is missing."
" Please specify this parameter in task or"
" export environment variable like 'export VMWARE_USER=ESXI_USERNAME'")

if not password:
module.fail_json(msg="Password parameter is missing."
" Please specify this parameter in task or"
" export environment variable like 'export VMWARE_PASSWORD=ESXI_PASSWORD'")
_raise_or_fail(msg="Password parameter is missing."
" Please specify this parameter in task or"
" export environment variable like 'export VMWARE_PASSWORD=ESXI_PASSWORD'")

if validate_certs and not hasattr(ssl, 'SSLContext'):
module.fail_json(msg='pyVim does not support changing verification mode with python < 2.7.9. Either update '
'python or use validate_certs=false.')
_raise_or_fail(msg='pyVim does not support changing verification mode with python < 2.7.9. Either update '
'python or use validate_certs=false.')
elif validate_certs:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
ssl_context.verify_mode = ssl.CERT_REQUIRED
Expand All @@ -706,8 +727,6 @@ def connect_to_api(module, disconnect_atexit=True, return_si=False, hostname=Non
ssl_context = None

service_instance = None
proxy_host = module.params.get('proxy_host')
proxy_port = module.params.get('proxy_port')

connect_args = dict(
host=hostname,
Expand All @@ -718,9 +737,9 @@ def connect_to_api(module, disconnect_atexit=True, return_si=False, hostname=Non

msg_suffix = ''
try:
if proxy_host:
msg_suffix = " [proxy: %s:%d]" % (proxy_host, proxy_port)
connect_args.update(httpProxyHost=proxy_host, httpProxyPort=proxy_port)
if httpProxyHost:
msg_suffix = " [proxy: %s:%d]" % (httpProxyHost, httpProxyPort)
connect_args.update(httpProxyHost=httpProxyHost, httpProxyPort=httpProxyPort)
smart_stub = connect.SmartStubAdapter(**connect_args)
session_stub = connect.VimSessionOrientedStub(smart_stub, connect.VimSessionOrientedStub.makeUserLoginMethod(username, password))
service_instance = vim.ServiceInstance('ServiceInstance', session_stub)
Expand All @@ -729,23 +748,23 @@ def connect_to_api(module, disconnect_atexit=True, return_si=False, hostname=Non
service_instance = connect.SmartConnect(**connect_args)
except vim.fault.InvalidLogin as invalid_login:
msg = "Unable to log on to vCenter or ESXi API at %s:%s " % (hostname, port)
module.fail_json(msg="%s as %s: %s" % (msg, username, invalid_login.msg) + msg_suffix)
_raise_or_fail(msg="%s as %s: %s" % (msg, username, invalid_login.msg) + msg_suffix)
except vim.fault.NoPermission as no_permission:
module.fail_json(msg="User %s does not have required permission"
" to log on to vCenter or ESXi API at %s:%s : %s" % (username, hostname, port, no_permission.msg))
_raise_or_fail(msg="User %s does not have required permission"
" to log on to vCenter or ESXi API at %s:%s : %s" % (username, hostname, port, no_permission.msg))
except (requests.ConnectionError, ssl.SSLError) as generic_req_exc:
module.fail_json(msg="Unable to connect to vCenter or ESXi API at %s on TCP/%s: %s" % (hostname, port, generic_req_exc))
_raise_or_fail(msg="Unable to connect to vCenter or ESXi API at %s on TCP/%s: %s" % (hostname, port, generic_req_exc))
except vmodl.fault.InvalidRequest as invalid_request:
# Request is malformed
msg = "Failed to get a response from server %s:%s " % (hostname, port)
module.fail_json(msg="%s as request is malformed: %s" % (msg, invalid_request.msg) + msg_suffix)
_raise_or_fail(msg="%s as request is malformed: %s" % (msg, invalid_request.msg) + msg_suffix)
except Exception as generic_exc:
msg = "Unknown error while connecting to vCenter or ESXi API at %s:%s" % (hostname, port) + msg_suffix
module.fail_json(msg="%s : %s" % (msg, generic_exc))
_raise_or_fail(msg="%s : %s" % (msg, generic_exc))

if service_instance is None:
msg = "Unknown error while connecting to vCenter or ESXi API at %s:%s" % (hostname, port)
module.fail_json(msg=msg + msg_suffix)
_raise_or_fail(msg=msg + msg_suffix)

# Disabling atexit should be used in special cases only.
# Such as IP change of the ESXi host which removes the connection anyway.
Expand Down

0 comments on commit c859996

Please sign in to comment.