VMware: add support for http_proxy in connection API
This commit allows users to access a vCenter or a ESXi through a HTTP CONNECT based proxy. To do so, the users have to set the `proxy_host` and `proxy_port` variables. The can also use the `VMWARE_PROXY_HOST` and `VMWARE_PROXY_PORT` environment variables. This feature depends on pyvmomi > v6.7.1.2018.12. Fixes: #42221 Co-Author: Abhijeet Kasurde <akasurde@redhat.com> Co-Author: Gonéri Le Bouder <goneri@redhat.com>
This commit is contained in:
parent
4eb156b2f5
commit
025e30ea0c
5 changed files with 127 additions and 36 deletions
2
changelogs/fragments/52936-vmware-proxy-support.yaml
Normal file
2
changelogs/fragments/52936-vmware-proxy-support.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- vmware - The VMware modules can now access a server behind a HTTP proxy (https://github.com/ansible/ansible/pull/52936)
|
|
@ -40,7 +40,9 @@ except ImportError:
|
||||||
|
|
||||||
from ansible.module_utils._text import to_text, to_native
|
from ansible.module_utils._text import to_text, to_native
|
||||||
from ansible.module_utils.six import integer_types, iteritems, string_types, raise_from
|
from ansible.module_utils.six import integer_types, iteritems, string_types, raise_from
|
||||||
|
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||||
from ansible.module_utils.basic import env_fallback, missing_required_lib
|
from ansible.module_utils.basic import env_fallback, missing_required_lib
|
||||||
|
from ansible.module_utils.urls import generic_urlparse
|
||||||
|
|
||||||
|
|
||||||
class TaskError(Exception):
|
class TaskError(Exception):
|
||||||
|
@ -486,7 +488,16 @@ def vmware_argument_spec():
|
||||||
validate_certs=dict(type='bool',
|
validate_certs=dict(type='bool',
|
||||||
required=False,
|
required=False,
|
||||||
default=True,
|
default=True,
|
||||||
fallback=(env_fallback, ['VMWARE_VALIDATE_CERTS'])),
|
fallback=(env_fallback, ['VMWARE_VALIDATE_CERTS'])
|
||||||
|
),
|
||||||
|
proxy_host=dict(type='str',
|
||||||
|
required=False,
|
||||||
|
default=None,
|
||||||
|
fallback=(env_fallback, ['VMWARE_PROXY_HOST'])),
|
||||||
|
proxy_port=dict(type='int',
|
||||||
|
required=False,
|
||||||
|
default=None,
|
||||||
|
fallback=(env_fallback, ['VMWARE_PROXY_PORT'])),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -523,18 +534,30 @@ def connect_to_api(module, disconnect_atexit=True, return_si=False):
|
||||||
ssl_context.load_default_certs()
|
ssl_context.load_default_certs()
|
||||||
|
|
||||||
service_instance = None
|
service_instance = None
|
||||||
|
proxy_host = module.params.get('proxy_host')
|
||||||
|
proxy_port = module.params.get('proxy_port')
|
||||||
|
|
||||||
|
connect_args = dict(
|
||||||
|
host=hostname,
|
||||||
|
port=port,
|
||||||
|
)
|
||||||
|
if ssl_context:
|
||||||
|
connect_args.update(sslContext=ssl_context)
|
||||||
|
|
||||||
|
msg_suffix = ''
|
||||||
try:
|
try:
|
||||||
connect_args = dict(
|
if proxy_host:
|
||||||
host=hostname,
|
msg_suffix = " [proxy: %s:%d]" % (proxy_host, proxy_port)
|
||||||
user=username,
|
connect_args.update(httpProxyHost=proxy_host, httpProxyPort=proxy_port)
|
||||||
pwd=password,
|
smart_stub = connect.SmartStubAdapter(**connect_args)
|
||||||
port=port,
|
session_stub = connect.VimSessionOrientedStub(smart_stub, connect.VimSessionOrientedStub.makeUserLoginMethod(username, password))
|
||||||
)
|
service_instance = vim.ServiceInstance('ServiceInstance', session_stub)
|
||||||
if ssl_context:
|
else:
|
||||||
connect_args.update(sslContext=ssl_context)
|
connect_args.update(user=username, pwd=password)
|
||||||
service_instance = connect.SmartConnect(**connect_args)
|
service_instance = connect.SmartConnect(**connect_args)
|
||||||
except vim.fault.InvalidLogin as invalid_login:
|
except vim.fault.InvalidLogin as invalid_login:
|
||||||
module.fail_json(msg="Unable to log on to vCenter or ESXi API at %s:%s as %s: %s" % (hostname, port, username, invalid_login.msg))
|
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)
|
||||||
except vim.fault.NoPermission as no_permission:
|
except vim.fault.NoPermission as no_permission:
|
||||||
module.fail_json(msg="User %s does not have required 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))
|
" to log on to vCenter or ESXi API at %s:%s : %s" % (username, hostname, port, no_permission.msg))
|
||||||
|
@ -542,13 +565,15 @@ def connect_to_api(module, disconnect_atexit=True, return_si=False):
|
||||||
module.fail_json(msg="Unable to connect to vCenter or ESXi API at %s on TCP/%s: %s" % (hostname, port, 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))
|
||||||
except vmodl.fault.InvalidRequest as invalid_request:
|
except vmodl.fault.InvalidRequest as invalid_request:
|
||||||
# Request is malformed
|
# Request is malformed
|
||||||
module.fail_json(msg="Failed to get a response from server %s:%s as "
|
msg = "Failed to get a response from server %s:%s " % (hostname, port)
|
||||||
"request is malformed: %s" % (hostname, port, invalid_request.msg))
|
module.fail_json(msg="%s as request is malformed: %s" % (msg, invalid_request.msg) + msg_suffix)
|
||||||
except Exception as generic_exc:
|
except Exception as generic_exc:
|
||||||
module.fail_json(msg="Unknown error while connecting to vCenter or ESXi API at %s:%s : %s" % (hostname, port, 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))
|
||||||
|
|
||||||
if service_instance is None:
|
if service_instance is None:
|
||||||
module.fail_json(msg="Unknown error while connecting to vCenter or ESXi API at %s:%s" % (hostname, port))
|
msg = "Unknown error while connecting to vCenter or ESXi API at %s:%s" % (hostname, port)
|
||||||
|
module.fail_json(msg=msg + msg_suffix)
|
||||||
|
|
||||||
# Disabling atexit should be used in special cases only.
|
# Disabling atexit should be used in special cases only.
|
||||||
# Such as IP change of the ESXi host which removes the connection anyway.
|
# Such as IP change of the ESXi host which removes the connection anyway.
|
||||||
|
|
|
@ -497,31 +497,45 @@ class VMwareHost(PyVmomi):
|
||||||
Function to return Host connection specification
|
Function to return Host connection specification
|
||||||
Returns: host connection specification
|
Returns: host connection specification
|
||||||
"""
|
"""
|
||||||
|
# Get the thumbprint of the SSL certificate
|
||||||
|
if self.fetch_ssl_thumbprint and self.esxi_ssl_thumbprint == '':
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(1)
|
||||||
|
if self.module.params['proxy_host']:
|
||||||
|
sock.connect((
|
||||||
|
self.module.params['proxy_host'],
|
||||||
|
self.module.params['proxy_port']))
|
||||||
|
command = "CONNECT %s:443 HTTP/1.0\r\n\r\n" % (self.esxi_hostname)
|
||||||
|
sock.send(command.encode())
|
||||||
|
buf = sock.recv(8192).decode()
|
||||||
|
if buf.split()[1] != '200':
|
||||||
|
self.module.fail_json(msg="Failed to connect to the proxy")
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
der_cert_bin = ctx.wrap_socket(sock, server_hostname=self.esxi_hostname).getpeercert(True)
|
||||||
|
sock.close()
|
||||||
|
else:
|
||||||
|
wrapped_socket = ssl.wrap_socket(sock)
|
||||||
|
try:
|
||||||
|
wrapped_socket.connect((self.esxi_hostname, 443))
|
||||||
|
except socket.error as socket_error:
|
||||||
|
self.module.fail_json(msg="Cannot connect to host : %s" % socket_error)
|
||||||
|
else:
|
||||||
|
der_cert_bin = wrapped_socket.getpeercert(True)
|
||||||
|
wrapped_socket.close()
|
||||||
|
|
||||||
|
thumb_sha1 = self.format_number(hashlib.sha1(der_cert_bin).hexdigest())
|
||||||
|
sslThumbprint = thumb_sha1
|
||||||
|
else:
|
||||||
|
sslThumbprint = self.esxi_ssl_thumbprint
|
||||||
|
|
||||||
host_connect_spec = vim.host.ConnectSpec()
|
host_connect_spec = vim.host.ConnectSpec()
|
||||||
|
host_connect_spec.sslThumbprint = sslThumbprint
|
||||||
host_connect_spec.hostName = self.esxi_hostname
|
host_connect_spec.hostName = self.esxi_hostname
|
||||||
host_connect_spec.userName = self.esxi_username
|
host_connect_spec.userName = self.esxi_username
|
||||||
host_connect_spec.password = self.esxi_password
|
host_connect_spec.password = self.esxi_password
|
||||||
host_connect_spec.force = self.force_connection
|
host_connect_spec.force = self.force_connection
|
||||||
# Get the thumbprint of the SSL certificate
|
|
||||||
if self.fetch_ssl_thumbprint and self.esxi_ssl_thumbprint == '':
|
|
||||||
# We need to grab the thumbprint manually because it's not included in
|
|
||||||
# the task error via an SSLVerifyFault exception anymore
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.settimeout(1)
|
|
||||||
wrapped_socket = ssl.wrap_socket(sock)
|
|
||||||
try:
|
|
||||||
wrapped_socket.connect((self.esxi_hostname, 443))
|
|
||||||
except socket.error as socket_error:
|
|
||||||
self.module.fail_json(msg="Cannot connect to host : %s" % socket_error)
|
|
||||||
else:
|
|
||||||
der_cert_bin = wrapped_socket.getpeercert(True)
|
|
||||||
# thumb_md5 = hashlib.md5(der_cert_bin).hexdigest()
|
|
||||||
thumb_sha1 = self.format_number(hashlib.sha1(der_cert_bin).hexdigest())
|
|
||||||
# thumb_sha256 = hashlib.sha256(der_cert_bin).hexdigest()
|
|
||||||
wrapped_socket.close()
|
|
||||||
host_connect_spec.sslThumbprint = thumb_sha1
|
|
||||||
else:
|
|
||||||
host_connect_spec.sslThumbprint = self.esxi_ssl_thumbprint
|
|
||||||
return host_connect_spec
|
return host_connect_spec
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -46,6 +46,22 @@ options:
|
||||||
type: int
|
type: int
|
||||||
default: 443
|
default: 443
|
||||||
version_added: '2.5'
|
version_added: '2.5'
|
||||||
|
proxy_host:
|
||||||
|
description:
|
||||||
|
- Address of a proxy that will receive all HTTPS requests and relay them.
|
||||||
|
- The format is a hostname or a IP.
|
||||||
|
- If the value is not specified in the task, the value of environment variable C(VMWARE_PROXY_HOST) will be used instead.
|
||||||
|
- This feature depends on a version of pyvmomi greater than v6.7.1.2018.12
|
||||||
|
type: str
|
||||||
|
version_added: '2.9'
|
||||||
|
required: False
|
||||||
|
proxy_port:
|
||||||
|
description:
|
||||||
|
- Port of the HTTP proxy that will receive all HTTPS requests and relay them.
|
||||||
|
- If the value is not specified in the task, the value of environment variable C(VMWARE_PROXY_PORT) will be used instead.
|
||||||
|
type: int
|
||||||
|
version_added: '2.9'
|
||||||
|
required: False
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# This doc fragment is specific to vcenter modules like vcenter_license
|
# This doc fragment is specific to vcenter modules like vcenter_license
|
||||||
|
@ -87,4 +103,19 @@ options:
|
||||||
type: int
|
type: int
|
||||||
default: 443
|
default: 443
|
||||||
version_added: '2.5'
|
version_added: '2.5'
|
||||||
|
proxy_host:
|
||||||
|
description:
|
||||||
|
- Address of a proxy that will receive all HTTPS requests and relay them.
|
||||||
|
- The format is a hostname or a IP.
|
||||||
|
- If the value is not specified in the task, the value of environment variable C(VMWARE_PROXY_HOST) will be used instead.
|
||||||
|
type: str
|
||||||
|
version_added: '2.9'
|
||||||
|
required: False
|
||||||
|
proxy_port:
|
||||||
|
description:
|
||||||
|
- Port of the HTTP proxy that will receive all HTTPS requests and relay them.
|
||||||
|
- If the value is not specified in the task, the value of environment variable C(VMWARE_PROXY_PORT) will be used instead.
|
||||||
|
type: int
|
||||||
|
version_added: '2.9'
|
||||||
|
required: False
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -54,6 +54,25 @@ test_data = [
|
||||||
),
|
),
|
||||||
"Unknown error while connecting to vCenter or ESXi API at esxi1:443"
|
"Unknown error while connecting to vCenter or ESXi API at esxi1:443"
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
dict(
|
||||||
|
username='Administrator@vsphere.local',
|
||||||
|
password='Esxi@123$%',
|
||||||
|
hostname='esxi1',
|
||||||
|
proxy_host='myproxyserver.com',
|
||||||
|
proxy_port=80,
|
||||||
|
validate_certs=False,
|
||||||
|
),
|
||||||
|
" [proxy: myproxyserver.com:80]"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
test_ids = [
|
||||||
|
'hostname',
|
||||||
|
'username',
|
||||||
|
'password',
|
||||||
|
'validate_certs',
|
||||||
|
'valid_http_proxy',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -115,7 +134,7 @@ def test_requests_lib_exists(mocker, fake_ansible_module):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info < (2, 7), reason="requires python2.7 and greater")
|
@pytest.mark.skipif(sys.version_info < (2, 7), reason="requires python2.7 and greater")
|
||||||
@pytest.mark.parametrize("params, msg", test_data, ids=['hostname', 'username', 'password', 'validate_certs'])
|
@pytest.mark.parametrize("params, msg", test_data, ids=test_ids)
|
||||||
def test_required_params(request, params, msg, fake_ansible_module):
|
def test_required_params(request, params, msg, fake_ansible_module):
|
||||||
""" Test if required params are correct or not"""
|
""" Test if required params are correct or not"""
|
||||||
fake_ansible_module.params = params
|
fake_ansible_module.params = params
|
||||||
|
|
Loading…
Reference in a new issue