Skip to content

Commit e259444

Browse files
committed
De-couple backend logic from module logic
1 parent 09c21d5 commit e259444

File tree

3 files changed

+136
-26
lines changed

3 files changed

+136
-26
lines changed
+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright: (c) 2015, Joseph Callen <jcallen () csc.com>
4+
# Copyright: (c) 2018, Ansible Project
5+
# Copyright: (c) 2018, James E. King III (@jeking3) <jking@apache.org>
6+
# Copyright: (c) 2025, Mario Lenz (@mariolenz) <m@riolenz.de>
7+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
8+
# SPDX-License-Identifier: GPL-3.0-or-later
9+
10+
__metaclass__ = type
11+
12+
import atexit
13+
import ssl
14+
import traceback
15+
16+
from ansible.module_utils.basic import missing_required_lib
17+
18+
REQUESTS_IMP_ERR = None
19+
try:
20+
# requests is required for exception handling of the ConnectionError
21+
import requests
22+
except ImportError:
23+
REQUESTS_IMP_ERR = traceback.format_exc()
24+
25+
PYVMOMI_IMP_ERR = None
26+
try:
27+
from pyVim import connect
28+
from pyVmomi import vim, vmodl
29+
except ImportError:
30+
PYVMOMI_IMP_ERR = traceback.format_exc()
31+
32+
33+
class MissingLibError(Exception):
34+
def __init__(self, library, exception, url=None):
35+
self.exception = exception
36+
self.library = library
37+
self.url = url
38+
super().__init__(missing_required_lib(self.library, url=self.url))
39+
40+
41+
class ApiAccessError(Exception):
42+
def __init__(self, *args, **kwargs):
43+
super(ApiAccessError, self).__init__(*args, **kwargs)
44+
45+
46+
class PyvmomiClient(object):
47+
def __init__(self, hostname, username, password, port=443, validate_certs=True, http_proxy_host=None, http_proxy_port=None):
48+
if REQUESTS_IMP_ERR:
49+
raise MissingLibError('requests', REQUESTS_IMP_ERR)
50+
51+
if PYVMOMI_IMP_ERR:
52+
raise MissingLibError('pyvmomi', PYVMOMI_IMP_ERR)
53+
54+
self.si, self.content = self._connect_to_api(hostname, username, password, port, validate_certs, http_proxy_host, http_proxy_port)
55+
56+
def _connect_to_api(self, hostname, username, password, port, validate_certs, http_proxy_host, http_proxy_port):
57+
if not hostname:
58+
raise ApiAccessError("Hostname parameter is missing.")
59+
60+
if not username:
61+
raise ApiAccessError("Username parameter is missing.")
62+
63+
if not password:
64+
raise ApiAccessError("Password parameter is missing.")
65+
66+
if validate_certs and not hasattr(ssl, 'SSLContext'):
67+
raise ApiAccessError('pyVim does not support changing verification mode with python < 2.7.9. Either update '
68+
'python or use validate_certs=false.')
69+
elif validate_certs:
70+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
71+
ssl_context.verify_mode = ssl.CERT_REQUIRED
72+
ssl_context.check_hostname = True
73+
ssl_context.load_default_certs()
74+
elif hasattr(ssl, 'SSLContext'):
75+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
76+
ssl_context.verify_mode = ssl.CERT_NONE
77+
ssl_context.check_hostname = False
78+
79+
service_instance = None
80+
81+
connect_args = dict(
82+
host=hostname,
83+
port=port,
84+
)
85+
if ssl_context:
86+
connect_args.update(sslContext=ssl_context)
87+
88+
msg_suffix = ''
89+
try:
90+
if http_proxy_host:
91+
msg_suffix = " [proxy: %s:%d]" % (http_proxy_host, http_proxy_port)
92+
connect_args.update(httpProxyHost=http_proxy_host, httpProxyPort=http_proxy_port)
93+
smart_stub = connect.SmartStubAdapter(**connect_args)
94+
session_stub = connect.VimSessionOrientedStub(smart_stub, connect.VimSessionOrientedStub.makeUserLoginMethod(username, password))
95+
service_instance = vim.ServiceInstance('ServiceInstance', session_stub)
96+
else:
97+
connect_args.update(user=username, pwd=password)
98+
service_instance = connect.SmartConnect(**connect_args)
99+
except vim.fault.InvalidLogin as invalid_login:
100+
msg = "Unable to log on to vCenter or ESXi API at %s:%s " % (hostname, port)
101+
raise ApiAccessError("%s as %s: %s" % (msg, username, invalid_login.msg) + msg_suffix)
102+
except vim.fault.NoPermission as no_permission:
103+
raise ApiAccessError("User %s does not have required permission"
104+
" to log on to vCenter or ESXi API at %s:%s : %s" % (username, hostname, port, no_permission.msg))
105+
except (requests.ConnectionError, ssl.SSLError) as generic_req_exc:
106+
raise ApiAccessError("Unable to connect to vCenter or ESXi API at %s on TCP/%s: %s" % (hostname, port, generic_req_exc))
107+
except vmodl.fault.InvalidRequest as invalid_request:
108+
# Request is malformed
109+
msg = "Failed to get a response from server %s:%s " % (hostname, port)
110+
raise ApiAccessError("%s as request is malformed: %s" % (msg, invalid_request.msg) + msg_suffix)
111+
except Exception as generic_exc:
112+
msg = "Unknown error while connecting to vCenter or ESXi API at %s:%s" % (hostname, port) + msg_suffix
113+
raise ApiAccessError("%s : %s" % (msg, generic_exc))
114+
115+
if service_instance is None:
116+
msg = "Unknown error while connecting to vCenter or ESXi API at %s:%s" % (hostname, port)
117+
raise ApiAccessError(msg + msg_suffix)
118+
119+
atexit.register(connect.Disconnect, service_instance)
120+
121+
return service_instance, service_instance.RetrieveContent()

plugins/module_utils/vmware.py

+11-12
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
77
# SPDX-License-Identifier: GPL-3.0-or-later
88

9-
from __future__ import absolute_import, division, print_function
109
__metaclass__ = type
1110

1211
import atexit
@@ -22,6 +21,7 @@
2221
import datetime
2322
from collections import OrderedDict
2423
from ansible.module_utils.compat.version import StrictVersion
24+
from ansible_collections.community.vmware.plugins.module_utils.clients._vmware import PyvmomiClient, ApiAccessError
2525
from random import randint
2626

2727

@@ -54,11 +54,6 @@ def __init__(self, *args, **kwargs):
5454
super(TaskError, self).__init__(*args, **kwargs)
5555

5656

57-
class ApiAccessError(Exception):
58-
def __init__(self, *args, **kwargs):
59-
super(ApiAccessError, self).__init__(*args, **kwargs)
60-
61-
6257
def check_answer_question_status(vm):
6358
"""Check whether locked a virtual machine.
6459
@@ -1067,11 +1062,9 @@ def quote_obj_name(object_name=None):
10671062
return object_name
10681063

10691064

1070-
class PyVmomi(object):
1065+
class PyVmomi(PyvmomiClient):
10711066
def __init__(self, module):
1072-
"""
1073-
Constructor
1074-
"""
1067+
self.module = module
10751068
if not HAS_REQUESTS:
10761069
module.fail_json(msg=missing_required_lib('requests'),
10771070
exception=REQUESTS_IMP_ERR)
@@ -1080,10 +1073,16 @@ def __init__(self, module):
10801073
module.fail_json(msg=missing_required_lib('PyVmomi'),
10811074
exception=PYVMOMI_IMP_ERR)
10821075

1083-
self.module = module
1076+
try:
1077+
super().__init__(module.params['hostname'], module.params['username'],
1078+
module.params['password'], module.params['port'],
1079+
module.params['validate_certs'],
1080+
module.params['proxy_host'],
1081+
module.params['proxy_port'])
1082+
except ApiAccessError as aae:
1083+
module.fail_json(msg=str(aae))
10841084
self.params = module.params
10851085
self.current_vm_obj = None
1086-
self.si, self.content = connect_to_api(self.module, return_si=True)
10871086
self.custom_field_mgr = []
10881087
if self.content.customFieldsManager: # not an ESXi
10891088
self.custom_field_mgr = self.content.customFieldsManager.field

tests/unit/module_utils/test_vmware.py

+4-14
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
username='Administrator@vsphere.local',
2626
password='Esxi@123$%',
2727
hostname=False,
28+
port=443,
2829
validate_certs=False,
2930
),
3031
"Hostname parameter is missing. Please specify this parameter in task or"
@@ -35,6 +36,7 @@
3536
username=False,
3637
password='Esxi@123$%',
3738
hostname='esxi1',
39+
port=443,
3840
validate_certs=False,
3941
),
4042
"Username parameter is missing. Please specify this parameter in task or"
@@ -45,6 +47,7 @@
4547
username='Administrator@vsphere.local',
4648
password=False,
4749
hostname='esxi1',
50+
port=443,
4851
validate_certs=False,
4952
),
5053
"Password parameter is missing. Please specify this parameter in task or"
@@ -64,6 +67,7 @@
6467
username='Administrator@vsphere.local',
6568
password='Esxi@123$%',
6669
hostname='esxi1',
70+
port=443,
6771
proxy_host='myproxyserver.com',
6872
proxy_port=80,
6973
validate_certs=False,
@@ -126,20 +130,6 @@ def test_required_params(request, params, msg, fake_ansible_module):
126130
# TODO: assert msg in fake_ansible_module.fail_json.call_args[1]['msg']
127131

128132

129-
def test_validate_certs(monkeypatch, fake_ansible_module):
130-
""" Test if SSL is required or not"""
131-
fake_ansible_module.params = test_data[3][0]
132-
133-
monkeypatch.setattr(vmware_module_utils, 'ssl', mock.Mock())
134-
del vmware_module_utils.ssl.SSLContext
135-
with pytest.raises(FailJsonException):
136-
vmware_module_utils.PyVmomi(fake_ansible_module)
137-
msg = 'pyVim does not support changing verification mode with python < 2.7.9.' \
138-
' Either update python or use validate_certs=false.'
139-
fake_ansible_module.fail_json.assert_called_once()
140-
assert msg == fake_ansible_module.fail_json.call_args[1]['msg']
141-
142-
143133
def test_vmdk_disk_path_split(monkeypatch, fake_ansible_module):
144134
""" Test vmdk_disk_path_split function"""
145135
fake_ansible_module.params = test_data[0][0]

0 commit comments

Comments
 (0)