diff --git a/ironic_python_agent/__init__.py b/ironic_python_agent/__init__.py index d42e7091c..1ed523071 100644 --- a/ironic_python_agent/__init__.py +++ b/ironic_python_agent/__init__.py @@ -12,8 +12,16 @@ import os +import eventlet + # NOTE(TheJulia): Eventlet, when monkey patching occurs, replaces the base # dns resolver methods. This can lead to compatability issues, # and un-expected exceptions being raised during the process # of monkey patching. Such as one if there are no resolvers. os.environ['EVENTLET_NO_GREENDNS'] = "yes" + +# NOTE(JayF) Without monkey_patching socket, API requests will hang with TLS +# enabled. Enabling more than just socket for monkey patching causes failures +# in image streaming. In an ideal world, we track down all those errors and +# monkey patch everything as suggested in eventlet documentation. +eventlet.monkey_patch(all=False, socket=True) diff --git a/ironic_python_agent/api/app.py b/ironic_python_agent/api/app.py index a4575ce7b..a379a7e04 100644 --- a/ironic_python_agent/api/app.py +++ b/ironic_python_agent/api/app.py @@ -130,7 +130,8 @@ class Application(object): """Start the API service in the background.""" self.service = wsgi.Server(self._conf, 'ironic-python-agent', app=self, host=self.agent.listen_address.hostname, - port=self.agent.listen_address.port) + port=self.agent.listen_address.port, + use_ssl=self._conf.listen_tls) self.service.start() LOG.info('Started API service on port %s', self.agent.listen_address.port) diff --git a/ironic_python_agent/config.py b/ironic_python_agent/config.py index 2c8105b11..16372c97f 100644 --- a/ironic_python_agent/config.py +++ b/ironic_python_agent/config.py @@ -54,6 +54,18 @@ cli_opts = [ help='The port to listen on. ' 'Can be supplied as "ipa-listen-port" kernel parameter.'), + # This is intentionally not settable via kernel command line, as it + # requires configuration parameters from oslo_service which are not + # configurable over the command line and require files-on-disk. + # Operators who want to use this support should configure it statically + # as part of a ramdisk build. + cfg.BoolOpt('listen_tls', + default=False, + help='When true, IPA will host API behind TLS. You will also ' + 'need to configure [ssl] group options for cert_file, ' + 'key_file, and, if desired, ca_file to validate client ' + 'certificates.'), + cfg.StrOpt('advertise_host', default=APARAMS.get('ipa-advertise-host', None), help='The host to tell Ironic to reply and send ' diff --git a/ironic_python_agent/tests/unit/test_agent.py b/ironic_python_agent/tests/unit/test_agent.py index fdcd17960..013eb83d9 100644 --- a/ironic_python_agent/tests/unit/test_agent.py +++ b/ironic_python_agent/tests/unit/test_agent.py @@ -208,7 +208,51 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest): mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) + wsgi_server.start.assert_called_once_with() + mock_wait.assert_called_once_with(mock.ANY) + self.assertEqual([mock.call('list_hardware_info'), + mock.call('wait_for_disks')], + mock_dispatch.call_args_list) + self.agent.heartbeater.start.assert_called_once_with() + + @mock.patch( + 'ironic_python_agent.hardware_managers.cna._detect_cna_card', + mock.Mock()) + @mock.patch.object(hardware, 'dispatch_to_managers', autospec=True) + @mock.patch.object(agent.IronicPythonAgent, + '_wait_for_interface', autospec=True) + @mock.patch('oslo_service.wsgi.Server', autospec=True) + @mock.patch.object(hardware, 'get_managers', autospec=True) + def test_run_with_ssl(self, mock_get_managers, mock_wsgi, + mock_wait, mock_dispatch): + CONF.set_override('inspection_callback_url', '') + CONF.set_override('listen_tls', True) + + wsgi_server = mock_wsgi.return_value + + def set_serve_api(): + self.agent.serve_api = False + + wsgi_server.start.side_effect = set_serve_api + self.agent.heartbeater = mock.Mock() + self.agent.api_client.lookup_node = mock.Mock() + self.agent.api_client.lookup_node.return_value = { + 'node': { + 'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c' + }, + 'config': { + 'heartbeat_timeout': 300 + } + } + + self.agent.run() + + mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', + app=self.agent.api, + host=mock.ANY, port=9999, + use_ssl=True) wsgi_server.start.assert_called_once_with() mock_wait.assert_called_once_with(mock.ANY) self.assertEqual([mock.call('list_hardware_info'), @@ -262,7 +306,8 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest): mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) wsgi_server.start.assert_called_once_with() mock_wait.assert_called_once_with(mock.ANY) self.assertEqual([mock.call('list_hardware_info'), @@ -320,7 +365,8 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest): mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) wsgi_server.start.assert_called_once_with() mock_wait.assert_called_once_with(mock.ANY) self.assertEqual([mock.call('list_hardware_info'), @@ -365,7 +411,8 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest): mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) wsgi_server.start.assert_called_once_with() mock_wait.assert_called_once_with(mock.ANY) self.assertEqual([mock.call('list_hardware_info'), @@ -412,7 +459,8 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest): mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, host='2001:db8:dead:beef::cafe', - port=9998) + port=9998, + use_ssl=False) wsgi_server.start.assert_called_once_with() mock_wait.assert_called_once_with(mock.ANY) self.assertEqual([mock.call('list_hardware_info'), @@ -455,7 +503,8 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest): mock_dispatch.call_args_list) mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) wsgi_server.start.assert_called_once_with() self.agent.heartbeater.start.assert_called_once_with() @@ -494,7 +543,8 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest): mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) wsgi_server.start.assert_called_once_with() mock_inspector.assert_called_once_with() @@ -557,7 +607,8 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest): mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) wsgi_server.start.assert_called_once_with() mock_inspector.assert_called_once_with() @@ -613,7 +664,8 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest): mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) wsgi_server.start.assert_called_once_with() self.assertFalse(mock_inspector.called) @@ -674,7 +726,8 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest): mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) wsgi_server.start.assert_called_once_with() self.agent.heartbeater.start.assert_called_once_with() @@ -827,7 +880,8 @@ class TestAgentStandalone(ironic_agent_base.IronicAgentTest): self.assertTrue(mock_get_managers.called) mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) wsgi_server_request.start.assert_called_once_with() self.assertFalse(self.agent.heartbeater.called) @@ -1051,7 +1105,8 @@ class TestBaseAgentVMediaToken(ironic_agent_base.IronicAgentTest): mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent', app=self.agent.api, - host=mock.ANY, port=9999) + host=mock.ANY, port=9999, + use_ssl=False) wsgi_server.start.assert_called_once_with() mock_wait.assert_called_once_with(mock.ANY) self.assertEqual([mock.call('list_hardware_info'), diff --git a/releasenotes/notes/add-optional-tls-support-3ab6a834154fedec.yaml b/releasenotes/notes/add-optional-tls-support-3ab6a834154fedec.yaml new file mode 100644 index 000000000..43daf57aa --- /dev/null +++ b/releasenotes/notes/add-optional-tls-support-3ab6a834154fedec.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Enables support in IPA for hosting the API server over TLS. Using this + support requires setting ``[DEFAULT]listen_tls`` to True, and then setting + ``[ssl]cert_file``, ``[ssl]key_file``, and optionally ``[ssl]ca_file`` to + files embedded in the ramdisk IPA runs inside. +