Skip to content

Commit 1d11f0b

Browse files
author
Jay Faulkner
committed
If listen_tls is true, enable TLS on wsgi server
This change enables operators to set [DEFAULT]listen_tls to true configure IPA to be host its WSGI server over TLS using existing SSL support in oslo.service. In addition to configuring this in IPA, a deployer will need to also set [ssl]cert_file, [ssl]key_file, and optionally [ssl]ca_file in their ipa config, in addition to embedding those files into the IPA ramdisk in order for this to be functional. In order to make this change work, we also need to monkey patch socket library early, or else oslo.service will end up passing an unpatched socket to the eventlet wsgi server, which causes deadlocks. Change-Id: Ib7decae410915f3c27b045ee08538c94d455b030
1 parent 7d0ad36 commit 1d11f0b

5 files changed

Lines changed: 97 additions & 13 deletions

File tree

ironic_python_agent/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,16 @@
1212

1313
import os
1414

15+
import eventlet
16+
1517
# NOTE(TheJulia): Eventlet, when monkey patching occurs, replaces the base
1618
# dns resolver methods. This can lead to compatability issues,
1719
# and un-expected exceptions being raised during the process
1820
# of monkey patching. Such as one if there are no resolvers.
1921
os.environ['EVENTLET_NO_GREENDNS'] = "yes"
22+
23+
# NOTE(JayF) Without monkey_patching socket, API requests will hang with TLS
24+
# enabled. Enabling more than just socket for monkey patching causes failures
25+
# in image streaming. In an ideal world, we track down all those errors and
26+
# monkey patch everything as suggested in eventlet documentation.
27+
eventlet.monkey_patch(all=False, socket=True)

ironic_python_agent/api/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ def start(self):
130130
"""Start the API service in the background."""
131131
self.service = wsgi.Server(self._conf, 'ironic-python-agent', app=self,
132132
host=self.agent.listen_address.hostname,
133-
port=self.agent.listen_address.port)
133+
port=self.agent.listen_address.port,
134+
use_ssl=self._conf.listen_tls)
134135
self.service.start()
135136
LOG.info('Started API service on port %s',
136137
self.agent.listen_address.port)

ironic_python_agent/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@
5454
help='The port to listen on. '
5555
'Can be supplied as "ipa-listen-port" kernel parameter.'),
5656

57+
# This is intentionally not settable via kernel command line, as it
58+
# requires configuration parameters from oslo_service which are not
59+
# configurable over the command line and require files-on-disk.
60+
# Operators who want to use this support should configure it statically
61+
# as part of a ramdisk build.
62+
cfg.BoolOpt('listen_tls',
63+
default=False,
64+
help='When true, IPA will host API behind TLS. You will also '
65+
'need to configure [ssl] group options for cert_file, '
66+
'key_file, and, if desired, ca_file to validate client '
67+
'certificates.'),
68+
5769
cfg.StrOpt('advertise_host',
5870
default=APARAMS.get('ipa-advertise-host', None),
5971
help='The host to tell Ironic to reply and send '

ironic_python_agent/tests/unit/test_agent.py

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,51 @@ def set_serve_api():
208208

209209
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
210210
app=self.agent.api,
211-
host=mock.ANY, port=9999)
211+
host=mock.ANY, port=9999,
212+
use_ssl=False)
213+
wsgi_server.start.assert_called_once_with()
214+
mock_wait.assert_called_once_with(mock.ANY)
215+
self.assertEqual([mock.call('list_hardware_info'),
216+
mock.call('wait_for_disks')],
217+
mock_dispatch.call_args_list)
218+
self.agent.heartbeater.start.assert_called_once_with()
219+
220+
@mock.patch(
221+
'ironic_python_agent.hardware_managers.cna._detect_cna_card',
222+
mock.Mock())
223+
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
224+
@mock.patch.object(agent.IronicPythonAgent,
225+
'_wait_for_interface', autospec=True)
226+
@mock.patch('oslo_service.wsgi.Server', autospec=True)
227+
@mock.patch.object(hardware, 'get_managers', autospec=True)
228+
def test_run_with_ssl(self, mock_get_managers, mock_wsgi,
229+
mock_wait, mock_dispatch):
230+
CONF.set_override('inspection_callback_url', '')
231+
CONF.set_override('listen_tls', True)
232+
233+
wsgi_server = mock_wsgi.return_value
234+
235+
def set_serve_api():
236+
self.agent.serve_api = False
237+
238+
wsgi_server.start.side_effect = set_serve_api
239+
self.agent.heartbeater = mock.Mock()
240+
self.agent.api_client.lookup_node = mock.Mock()
241+
self.agent.api_client.lookup_node.return_value = {
242+
'node': {
243+
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
244+
},
245+
'config': {
246+
'heartbeat_timeout': 300
247+
}
248+
}
249+
250+
self.agent.run()
251+
252+
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
253+
app=self.agent.api,
254+
host=mock.ANY, port=9999,
255+
use_ssl=True)
212256
wsgi_server.start.assert_called_once_with()
213257
mock_wait.assert_called_once_with(mock.ANY)
214258
self.assertEqual([mock.call('list_hardware_info'),
@@ -262,7 +306,8 @@ def set_serve_api():
262306

263307
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
264308
app=self.agent.api,
265-
host=mock.ANY, port=9999)
309+
host=mock.ANY, port=9999,
310+
use_ssl=False)
266311
wsgi_server.start.assert_called_once_with()
267312
mock_wait.assert_called_once_with(mock.ANY)
268313
self.assertEqual([mock.call('list_hardware_info'),
@@ -320,7 +365,8 @@ def set_serve_api():
320365

321366
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
322367
app=self.agent.api,
323-
host=mock.ANY, port=9999)
368+
host=mock.ANY, port=9999,
369+
use_ssl=False)
324370
wsgi_server.start.assert_called_once_with()
325371
mock_wait.assert_called_once_with(mock.ANY)
326372
self.assertEqual([mock.call('list_hardware_info'),
@@ -365,7 +411,8 @@ def set_serve_api():
365411

366412
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
367413
app=self.agent.api,
368-
host=mock.ANY, port=9999)
414+
host=mock.ANY, port=9999,
415+
use_ssl=False)
369416
wsgi_server.start.assert_called_once_with()
370417
mock_wait.assert_called_once_with(mock.ANY)
371418
self.assertEqual([mock.call('list_hardware_info'),
@@ -412,7 +459,8 @@ def set_serve_api():
412459
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
413460
app=self.agent.api,
414461
host='2001:db8:dead:beef::cafe',
415-
port=9998)
462+
port=9998,
463+
use_ssl=False)
416464
wsgi_server.start.assert_called_once_with()
417465
mock_wait.assert_called_once_with(mock.ANY)
418466
self.assertEqual([mock.call('list_hardware_info'),
@@ -455,7 +503,8 @@ def test_run_raise_keyboard_interrupt(self, mock_get_managers, mock_wsgi,
455503
mock_dispatch.call_args_list)
456504
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
457505
app=self.agent.api,
458-
host=mock.ANY, port=9999)
506+
host=mock.ANY, port=9999,
507+
use_ssl=False)
459508
wsgi_server.start.assert_called_once_with()
460509
self.agent.heartbeater.start.assert_called_once_with()
461510

@@ -494,7 +543,8 @@ def set_serve_api():
494543

495544
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
496545
app=self.agent.api,
497-
host=mock.ANY, port=9999)
546+
host=mock.ANY, port=9999,
547+
use_ssl=False)
498548
wsgi_server.start.assert_called_once_with()
499549

500550
mock_inspector.assert_called_once_with()
@@ -557,7 +607,8 @@ def set_serve_api():
557607

558608
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
559609
app=self.agent.api,
560-
host=mock.ANY, port=9999)
610+
host=mock.ANY, port=9999,
611+
use_ssl=False)
561612
wsgi_server.start.assert_called_once_with()
562613

563614
mock_inspector.assert_called_once_with()
@@ -613,7 +664,8 @@ def set_serve_api():
613664

614665
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
615666
app=self.agent.api,
616-
host=mock.ANY, port=9999)
667+
host=mock.ANY, port=9999,
668+
use_ssl=False)
617669
wsgi_server.start.assert_called_once_with()
618670

619671
self.assertFalse(mock_inspector.called)
@@ -674,7 +726,8 @@ def set_serve_api():
674726

675727
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
676728
app=self.agent.api,
677-
host=mock.ANY, port=9999)
729+
host=mock.ANY, port=9999,
730+
use_ssl=False)
678731
wsgi_server.start.assert_called_once_with()
679732

680733
self.agent.heartbeater.start.assert_called_once_with()
@@ -827,7 +880,8 @@ def set_serve_api():
827880
self.assertTrue(mock_get_managers.called)
828881
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
829882
app=self.agent.api,
830-
host=mock.ANY, port=9999)
883+
host=mock.ANY, port=9999,
884+
use_ssl=False)
831885
wsgi_server_request.start.assert_called_once_with()
832886

833887
self.assertFalse(self.agent.heartbeater.called)
@@ -1051,7 +1105,8 @@ def set_serve_api():
10511105

10521106
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
10531107
app=self.agent.api,
1054-
host=mock.ANY, port=9999)
1108+
host=mock.ANY, port=9999,
1109+
use_ssl=False)
10551110
wsgi_server.start.assert_called_once_with()
10561111
mock_wait.assert_called_once_with(mock.ANY)
10571112
self.assertEqual([mock.call('list_hardware_info'),
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
features:
3+
- |
4+
Enables support in IPA for hosting the API server over TLS. Using this
5+
support requires setting ``[DEFAULT]listen_tls`` to True, and then setting
6+
``[ssl]cert_file``, ``[ssl]key_file``, and optionally ``[ssl]ca_file`` to
7+
files embedded in the ramdisk IPA runs inside.
8+

0 commit comments

Comments
 (0)