Python < 2.7.9 does not have the ssl.SSLContext attribute.
ssl.SSLContext is only required when we want to validate the SSL
connection. If `validate_certs` is false, we don't initialize the
`ssl_context` variable.
Add unit-test coverage and a little refactoring:
- avoid the use of `mocker`, when we can push `monkeypatch` which is
`pytest`'s default.
- use `mock.Mocker()` when possible
closes: #57072
(cherry picked from commit 3ea8e0a14417168be7ada4ed73e6dbc7bb3e4782)
... | ... |
@@ -513,12 +513,17 @@ def connect_to_api(module, disconnect_atexit=True): |
513 | 513 |
if validate_certs and not hasattr(ssl, 'SSLContext'): |
514 | 514 |
module.fail_json(msg='pyVim does not support changing verification mode with python < 2.7.9. Either update ' |
515 | 515 |
'python or use validate_certs=false.') |
516 |
- |
|
517 |
- ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) |
|
518 |
- if validate_certs: |
|
516 |
+ elif validate_certs: |
|
517 |
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) |
|
519 | 518 |
ssl_context.verify_mode = ssl.CERT_REQUIRED |
520 | 519 |
ssl_context.check_hostname = True |
521 | 520 |
ssl_context.load_default_certs() |
521 |
+ elif hasattr(ssl, 'SSLContext'): |
|
522 |
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) |
|
523 |
+ ssl_context.verify_mode = ssl.CERT_NONE |
|
524 |
+ ssl_context.check_hostname = False |
|
525 |
+ else: # Python < 2.7.9 or RHEL/Centos < 7.4 |
|
526 |
+ ssl_context = None |
|
522 | 527 |
|
523 | 528 |
service_instance = None |
524 | 529 |
try: |
... | ... |
@@ -6,12 +6,15 @@ |
6 | 6 |
from __future__ import absolute_import, division, print_function |
7 | 7 |
__metaclass__ = type |
8 | 8 |
|
9 |
+import ssl |
|
9 | 10 |
import sys |
10 | 11 |
import pytest |
11 | 12 |
|
12 | 13 |
pyvmomi = pytest.importorskip('pyVmomi') |
13 | 14 |
|
14 |
-from ansible.module_utils.vmware import connect_to_api, PyVmomi |
|
15 |
+from units.compat import mock |
|
16 |
+ |
|
17 |
+import ansible.module_utils.vmware as vmware_module_utils |
|
15 | 18 |
|
16 | 19 |
|
17 | 20 |
test_data = [ |
... | ... |
@@ -57,59 +60,38 @@ test_data = [ |
57 | 57 |
] |
58 | 58 |
|
59 | 59 |
|
60 |
-class AnsibleModuleExit(Exception): |
|
61 |
- def __init__(self, *args, **kwargs): |
|
62 |
- self.args = args |
|
63 |
- self.kwargs = kwargs |
|
64 |
- |
|
65 |
- |
|
66 |
-class ExitJson(AnsibleModuleExit): |
|
67 |
- pass |
|
68 |
- |
|
69 |
- |
|
70 |
-class FailJson(AnsibleModuleExit): |
|
60 |
+class FailJsonException(BaseException): |
|
71 | 61 |
pass |
72 | 62 |
|
73 | 63 |
|
74 | 64 |
@pytest.fixture |
75 | 65 |
def fake_ansible_module(): |
76 |
- return FakeAnsibleModule() |
|
77 |
- |
|
78 |
- |
|
79 |
-class FakeAnsibleModule: |
|
80 |
- def __init__(self): |
|
81 |
- self.params = {} |
|
82 |
- self.tmpdir = None |
|
66 |
+ ret = mock.Mock() |
|
67 |
+ ret.params = test_data[3][0] |
|
68 |
+ ret.tmpdir = None |
|
69 |
+ ret.fail_json.side_effect = FailJsonException() |
|
70 |
+ return ret |
|
83 | 71 |
|
84 |
- def exit_json(self, *args, **kwargs): |
|
85 |
- raise ExitJson(*args, **kwargs) |
|
86 | 72 |
|
87 |
- def fail_json(self, *args, **kwargs): |
|
88 |
- raise FailJson(*args, **kwargs) |
|
73 |
+def fake_connect_to_api(module, return_si=None): |
|
74 |
+ return mock.Mock() |
|
89 | 75 |
|
90 | 76 |
|
91 |
-def fake_connect_to_api(module): |
|
92 |
- class MyContent(): |
|
93 |
- customFieldsManager = None |
|
94 |
- return MyContent |
|
77 |
+testdata = [ |
|
78 |
+ ('HAS_PYVMOMI', 'PyVmomi'), |
|
79 |
+ ('HAS_REQUESTS', 'requests'), |
|
80 |
+] |
|
95 | 81 |
|
96 | 82 |
|
97 |
-def test_pyvmomi_lib_exists(mocker, fake_ansible_module): |
|
83 |
+@pytest.mark.parametrize("key,libname", testdata) |
|
84 |
+def test_lib_loading_failure(monkeypatch, fake_ansible_module, key, libname): |
|
98 | 85 |
""" Test if Pyvmomi is present or not""" |
99 |
- mocker.patch('ansible.module_utils.vmware.HAS_PYVMOMI', new=False) |
|
100 |
- with pytest.raises(FailJson) as exec_info: |
|
101 |
- PyVmomi(fake_ansible_module) |
|
102 |
- |
|
103 |
- assert 'Failed to import the required Python library (PyVmomi) on' in exec_info.value.kwargs['msg'] |
|
104 |
- |
|
105 |
- |
|
106 |
-def test_requests_lib_exists(mocker, fake_ansible_module): |
|
107 |
- """ Test if requests is present or not""" |
|
108 |
- mocker.patch('ansible.module_utils.vmware.HAS_REQUESTS', new=False) |
|
109 |
- with pytest.raises(FailJson) as exec_info: |
|
110 |
- PyVmomi(fake_ansible_module) |
|
111 |
- |
|
112 |
- assert 'Failed to import the required Python library (requests) on' in exec_info.value.kwargs['msg'] |
|
86 |
+ monkeypatch.setattr(vmware_module_utils, key, False) |
|
87 |
+ with pytest.raises(FailJsonException): |
|
88 |
+ vmware_module_utils.PyVmomi(fake_ansible_module) |
|
89 |
+ error_str = 'Failed to import the required Python library (%s)' % libname |
|
90 |
+ assert fake_ansible_module.fail_json.called_once() |
|
91 |
+ assert error_str in fake_ansible_module.fail_json.call_args[1]['msg'] |
|
113 | 92 |
|
114 | 93 |
|
115 | 94 |
@pytest.mark.skipif(sys.version_info < (2, 7), reason="requires python2.7 and greater") |
... | ... |
@@ -117,40 +99,111 @@ def test_requests_lib_exists(mocker, fake_ansible_module): |
117 | 117 |
def test_required_params(request, params, msg, fake_ansible_module): |
118 | 118 |
""" Test if required params are correct or not""" |
119 | 119 |
fake_ansible_module.params = params |
120 |
- with pytest.raises(FailJson) as exec_info: |
|
121 |
- connect_to_api(fake_ansible_module) |
|
122 |
- assert msg in exec_info.value.kwargs['msg'] |
|
120 |
+ with pytest.raises(FailJsonException): |
|
121 |
+ vmware_module_utils.connect_to_api(fake_ansible_module) |
|
122 |
+ assert fake_ansible_module.fail_json.called_once() |
|
123 |
+ assert msg in fake_ansible_module.fail_json.call_args[1]['msg'] |
|
123 | 124 |
|
124 | 125 |
|
125 |
-def test_validate_certs(mocker, fake_ansible_module): |
|
126 |
+def test_validate_certs(monkeypatch, fake_ansible_module): |
|
126 | 127 |
""" Test if SSL is required or not""" |
127 | 128 |
fake_ansible_module.params = test_data[3][0] |
128 | 129 |
|
129 |
- mocker.patch('ansible.module_utils.vmware.ssl', new=None) |
|
130 |
- with pytest.raises(FailJson) as exec_info: |
|
131 |
- PyVmomi(fake_ansible_module) |
|
130 |
+ monkeypatch.setattr(vmware_module_utils, 'ssl', None) |
|
131 |
+ with pytest.raises(FailJsonException): |
|
132 |
+ vmware_module_utils.PyVmomi(fake_ansible_module) |
|
132 | 133 |
msg = 'pyVim does not support changing verification mode with python < 2.7.9.' \ |
133 | 134 |
' Either update python or use validate_certs=false.' |
134 |
- assert msg == exec_info.value.kwargs['msg'] |
|
135 |
+ assert fake_ansible_module.fail_json.called_once() |
|
136 |
+ assert msg in fake_ansible_module.fail_json.call_args[1]['msg'] |
|
135 | 137 |
|
136 | 138 |
|
137 |
-def test_vmdk_disk_path_split(mocker, fake_ansible_module): |
|
139 |
+def test_vmdk_disk_path_split(monkeypatch, fake_ansible_module): |
|
138 | 140 |
""" Test vmdk_disk_path_split function""" |
139 | 141 |
fake_ansible_module.params = test_data[0][0] |
140 | 142 |
|
141 |
- mocker.patch('ansible.module_utils.vmware.connect_to_api', new=fake_connect_to_api) |
|
142 |
- pyv = PyVmomi(fake_ansible_module) |
|
143 |
+ monkeypatch.setattr(vmware_module_utils, 'connect_to_api', fake_connect_to_api) |
|
144 |
+ pyv = vmware_module_utils.PyVmomi(fake_ansible_module) |
|
143 | 145 |
v = pyv.vmdk_disk_path_split('[ds1] VM_0001/VM0001_0.vmdk') |
144 | 146 |
assert v == ('ds1', 'VM_0001/VM0001_0.vmdk', 'VM0001_0.vmdk', 'VM_0001') |
145 | 147 |
|
146 | 148 |
|
147 |
-def test_vmdk_disk_path_split_negative(mocker, fake_ansible_module): |
|
149 |
+def test_vmdk_disk_path_split_negative(monkeypatch, fake_ansible_module): |
|
148 | 150 |
""" Test vmdk_disk_path_split function""" |
149 | 151 |
fake_ansible_module.params = test_data[0][0] |
150 | 152 |
|
151 |
- mocker.patch('ansible.module_utils.vmware.connect_to_api', new=fake_connect_to_api) |
|
152 |
- with pytest.raises(FailJson) as exec_info: |
|
153 |
- pyv = PyVmomi(fake_ansible_module) |
|
153 |
+ monkeypatch.setattr(vmware_module_utils, 'connect_to_api', fake_connect_to_api) |
|
154 |
+ with pytest.raises(FailJsonException): |
|
155 |
+ pyv = vmware_module_utils.PyVmomi(fake_ansible_module) |
|
154 | 156 |
pyv.vmdk_disk_path_split('[ds1]') |
157 |
+ assert fake_ansible_module.fail_json.called_once() |
|
158 |
+ assert 'Bad path' in fake_ansible_module.fail_json.call_args[1]['msg'] |
|
159 |
+ |
|
155 | 160 |
|
156 |
- assert 'Bad path' in exec_info.value.kwargs['msg'] |
|
161 |
+@pytest.mark.skipif(sys.version_info < (2, 7), reason="requires python2.7 and greater") |
|
162 |
+def test_connect_to_api_validate_certs(monkeypatch, fake_ansible_module): |
|
163 |
+ monkeypatch.setattr(vmware_module_utils, 'connect', mock.Mock()) |
|
164 |
+ |
|
165 |
+ def MockSSLContext(proto): |
|
166 |
+ ssl_context.proto = proto |
|
167 |
+ return ssl_context |
|
168 |
+ |
|
169 |
+ # New Python with SSLContext + validate_certs=True |
|
170 |
+ vmware_module_utils.connect.reset_mock() |
|
171 |
+ ssl_context = mock.Mock() |
|
172 |
+ monkeypatch.setattr(vmware_module_utils.ssl, 'SSLContext', MockSSLContext) |
|
173 |
+ fake_ansible_module.params['validate_certs'] = True |
|
174 |
+ vmware_module_utils.connect_to_api(fake_ansible_module) |
|
175 |
+ assert ssl_context.proto == ssl.PROTOCOL_SSLv23 |
|
176 |
+ assert ssl_context.verify_mode == ssl.CERT_REQUIRED |
|
177 |
+ assert ssl_context.check_hostname is True |
|
178 |
+ vmware_module_utils.connect.SmartConnect.assert_called_once_with( |
|
179 |
+ host='esxi1', |
|
180 |
+ port=443, |
|
181 |
+ pwd='Esxi@123$%', |
|
182 |
+ user='Administrator@vsphere.local', |
|
183 |
+ sslContext=ssl_context) |
|
184 |
+ |
|
185 |
+ # New Python with SSLContext + validate_certs=False |
|
186 |
+ vmware_module_utils.connect.reset_mock() |
|
187 |
+ ssl_context = mock.Mock() |
|
188 |
+ monkeypatch.setattr(vmware_module_utils.ssl, 'SSLContext', MockSSLContext) |
|
189 |
+ fake_ansible_module.params['validate_certs'] = False |
|
190 |
+ vmware_module_utils.connect_to_api(fake_ansible_module) |
|
191 |
+ assert ssl_context.proto == ssl.PROTOCOL_SSLv23 |
|
192 |
+ assert ssl_context.verify_mode == ssl.CERT_NONE |
|
193 |
+ assert ssl_context.check_hostname is False |
|
194 |
+ vmware_module_utils.connect.SmartConnect.assert_called_once_with( |
|
195 |
+ host='esxi1', |
|
196 |
+ port=443, |
|
197 |
+ pwd='Esxi@123$%', |
|
198 |
+ user='Administrator@vsphere.local', |
|
199 |
+ sslContext=ssl_context) |
|
200 |
+ |
|
201 |
+ # Old Python with no SSLContext + validate_certs=True |
|
202 |
+ vmware_module_utils.connect.reset_mock() |
|
203 |
+ ssl_context = mock.Mock() |
|
204 |
+ ssl_context.proto = None |
|
205 |
+ monkeypatch.delattr(vmware_module_utils.ssl, 'SSLContext') |
|
206 |
+ fake_ansible_module.params['validate_certs'] = True |
|
207 |
+ with pytest.raises(FailJsonException): |
|
208 |
+ vmware_module_utils.connect_to_api(fake_ansible_module) |
|
209 |
+ assert ssl_context.proto is None |
|
210 |
+ fake_ansible_module.fail_json.assert_called_once_with(msg=( |
|
211 |
+ 'pyVim does not support changing verification mode with python ' |
|
212 |
+ '< 2.7.9. Either update python or use validate_certs=false.')) |
|
213 |
+ assert not vmware_module_utils.connect.SmartConnect.called |
|
214 |
+ |
|
215 |
+ # Old Python with no SSLContext + validate_certs=False |
|
216 |
+ vmware_module_utils.connect.reset_mock() |
|
217 |
+ ssl_context = mock.Mock() |
|
218 |
+ ssl_context.proto = None |
|
219 |
+ monkeypatch.delattr(vmware_module_utils.ssl, 'SSLContext', raising=False) |
|
220 |
+ fake_ansible_module.params['validate_certs'] = False |
|
221 |
+ vmware_module_utils.connect_to_api(fake_ansible_module) |
|
222 |
+ assert ssl_context.proto is None |
|
223 |
+ vmware_module_utils.connect.SmartConnect.assert_called_once_with( |
|
224 |
+ host='esxi1', |
|
225 |
+ port=443, |
|
226 |
+ pwd='Esxi@123$%', |
|
227 |
+ user='Administrator@vsphere.local') |