diff --git a/changelogs/fragments/52936-vmware-proxy-support.yaml b/changelogs/fragments/52936-vmware-proxy-support.yaml new file mode 100644 index 00000000000..507469e8525 --- /dev/null +++ b/changelogs/fragments/52936-vmware-proxy-support.yaml @@ -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) diff --git a/lib/ansible/module_utils/vmware.py b/lib/ansible/module_utils/vmware.py index 826f4e72443..5a90355cf97 100644 --- a/lib/ansible/module_utils/vmware.py +++ b/lib/ansible/module_utils/vmware.py @@ -40,7 +40,9 @@ except ImportError: 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.moves.urllib.parse import urlparse from ansible.module_utils.basic import env_fallback, missing_required_lib +from ansible.module_utils.urls import generic_urlparse class TaskError(Exception): @@ -486,7 +488,16 @@ def vmware_argument_spec(): validate_certs=dict(type='bool', required=False, 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() 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: - connect_args = dict( - host=hostname, - user=username, - pwd=password, - port=port, - ) - if ssl_context: - connect_args.update(sslContext=ssl_context) - service_instance = connect.SmartConnect(**connect_args) + if proxy_host: + msg_suffix = " [proxy: %s:%d]" % (proxy_host, proxy_port) + connect_args.update(httpProxyHost=proxy_host, httpProxyPort=proxy_port) + smart_stub = connect.SmartStubAdapter(**connect_args) + session_stub = connect.VimSessionOrientedStub(smart_stub, connect.VimSessionOrientedStub.makeUserLoginMethod(username, password)) + service_instance = vim.ServiceInstance('ServiceInstance', session_stub) + else: + connect_args.update(user=username, pwd=password) + service_instance = connect.SmartConnect(**connect_args) 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: 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)) @@ -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)) except vmodl.fault.InvalidRequest as invalid_request: # Request is malformed - module.fail_json(msg="Failed to get a response from server %s:%s as " - "request is malformed: %s" % (hostname, port, invalid_request.msg)) + 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) 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: - 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. # Such as IP change of the ESXi host which removes the connection anyway. diff --git a/lib/ansible/modules/cloud/vmware/vmware_host.py b/lib/ansible/modules/cloud/vmware/vmware_host.py index c6a5b6d40f5..c9a3311c1c5 100644 --- a/lib/ansible/modules/cloud/vmware/vmware_host.py +++ b/lib/ansible/modules/cloud/vmware/vmware_host.py @@ -497,31 +497,45 @@ class VMwareHost(PyVmomi): Function to return Host connection specification Returns: host connection specification """ - host_connect_spec = vim.host.ConnectSpec() - host_connect_spec.hostName = self.esxi_hostname - host_connect_spec.userName = self.esxi_username - host_connect_spec.password = self.esxi_password - 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) + 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: - 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 + 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: - host_connect_spec.sslThumbprint = self.esxi_ssl_thumbprint + sslThumbprint = self.esxi_ssl_thumbprint + + host_connect_spec = vim.host.ConnectSpec() + host_connect_spec.sslThumbprint = sslThumbprint + host_connect_spec.hostName = self.esxi_hostname + host_connect_spec.userName = self.esxi_username + host_connect_spec.password = self.esxi_password + host_connect_spec.force = self.force_connection return host_connect_spec @staticmethod diff --git a/lib/ansible/plugins/doc_fragments/vmware.py b/lib/ansible/plugins/doc_fragments/vmware.py index 99c6a5fa66d..fdc70d4c3fd 100644 --- a/lib/ansible/plugins/doc_fragments/vmware.py +++ b/lib/ansible/plugins/doc_fragments/vmware.py @@ -46,6 +46,22 @@ options: type: int default: 443 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 @@ -87,4 +103,19 @@ options: type: int default: 443 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 ''' diff --git a/test/units/module_utils/test_vmware.py b/test/units/module_utils/test_vmware.py index e19e305daad..e096fd753ca 100644 --- a/test/units/module_utils/test_vmware.py +++ b/test/units/module_utils/test_vmware.py @@ -54,6 +54,25 @@ test_data = [ ), "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.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): """ Test if required params are correct or not""" fake_ansible_module.params = params