Browse code

VMware: not ssl.SSLContext if validate_certs false (#57185)

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)

Gonéri Le Bouder authored on 2019/09/10 01:11:46
Showing 3 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+bugfixes:
1
+  - vmware - Ensure we can use the modules with Python < 2.7.9 or RHEL/CentOS < 7.4, this as soon as ``validate_certs`` is disabled.
... ...
@@ -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')