Add "none" RPC transport that disables the RPC bus

When using the new combined executable in a single-conductor scenario,
it may make sense to completely disable the remote RPC. The new
``rpc_transport`` value ``none`` achieves that.

Change-Id: I6a83358c65b3ed213c8a991d42660ca51fc3a8ec
Story: #2009676
Task: #44104
This commit is contained in:
Dmitry Tantsur 2021-11-29 11:36:06 +01:00
parent 9a6f2d101b
commit 019ed2d7b1
11 changed files with 123 additions and 16 deletions

View File

@ -92,6 +92,20 @@ You should make the following changes to ``/etc/ironic/ironic.conf``:
username = myName
password = myPassword
#. Starting with the Yoga release series, you can use a combined API+conductor
service and completely disable the RPC. Set
.. code-block:: ini
[DEFAULT]
rpc_transport = none
and use the ``ironic`` executable to start the combined service.
.. note::
The combined service also works with RPC enabled, which can be useful for
some deployments, but may not be advisable for all security models.
Using CLI
---------

View File

@ -30,6 +30,7 @@ def initialize_wsgi_app(argv=sys.argv):
i18n.install('ironic')
service.prepare_command(argv)
service.ensure_rpc_transport()
LOG.debug("Configuration:")
CONF.log_opt_values(LOG, log.DEBUG)

View File

@ -33,6 +33,7 @@ LOG = log.getLogger(__name__)
def main():
# Parse config file and command line options, then start logging
ironic_service.prepare_service('ironic_api', sys.argv)
ironic_service.ensure_rpc_transport()
# Build and start the WSGI app
launcher = ironic_service.process_launcher()

View File

@ -58,6 +58,7 @@ def main():
# Parse config file and command line options, then start logging
ironic_service.prepare_service('ironic_conductor', sys.argv)
ironic_service.ensure_rpc_transport(CONF)
mgr = rpc_service.RPCService(CONF.host,
'ironic.conductor.manager',

View File

@ -53,19 +53,22 @@ class RPCService(service.Service):
if CONF.rpc_transport == 'json-rpc':
self.rpcserver = json_rpc.WSGIService(
self.manager, serializer, context.RequestContext.from_dict)
else:
elif CONF.rpc_transport != 'none':
target = messaging.Target(topic=self.topic, server=self.host)
endpoints = [self.manager]
self.rpcserver = rpc.get_server(target, endpoints, serializer)
self.rpcserver.start()
if self.rpcserver is not None:
self.rpcserver.start()
self.handle_signal()
self.manager.init_host(admin_context)
rpc.set_global_manager(self.manager)
LOG.info('Created RPC server for service %(service)s on host '
'%(host)s.',
{'service': self.topic, 'host': self.host})
LOG.info('Created RPC server with %(transport)s transport for service '
'%(service)s on host %(host)s.',
{'service': self.topic, 'host': self.host,
'transport': CONF.rpc_transport})
def stop(self):
try:

View File

@ -69,3 +69,11 @@ def prepare_service(name, argv=None, conf=CONF):
def process_launcher():
return service.ProcessLauncher(CONF, restart_method='mutate')
def ensure_rpc_transport(conf=CONF):
# Only the combined ironic executable can use rpc_transport = none
if conf.rpc_transport == 'none':
raise RuntimeError("This service is not designed to work with "
"rpc_transport = none. Please use the combined "
"ironic executable or another RPC transport.")

View File

@ -174,10 +174,12 @@ class ConductorAPI(object):
self.client = json_rpc.Client(serializer=serializer,
version_cap=version_cap)
self.topic = ''
else:
elif CONF.rpc_transport != 'none':
target = messaging.Target(topic=self.topic, version='1.0')
self.client = rpc.get_client(target, version_cap=version_cap,
serializer=serializer)
else:
self.client = None
# NOTE(tenbrae): this is going to be buggy
self.ring_manager = hash_ring.HashRingManager()
@ -203,6 +205,13 @@ class ConductorAPI(object):
# conductor.
return _LOCAL_CONTEXT
# A safeguard for the case someone uses rpc_transport=None with no
# built-in conductor.
if self.client is None:
raise exception.ServiceUnavailable(
_("Cannot use 'none' RPC to connect to remote conductor %s")
% host)
# Normal RPC path
return self.client.prepare(topic=topic, version=version)
@ -276,13 +285,17 @@ class ConductorAPI(object):
"""Get RPC topic name for the current conductor."""
return self.topic + "." + CONF.host
def _can_send_version(self, version):
return (self.client.can_send_version(version)
if self.client is not None else True)
def can_send_create_port(self):
"""Return whether the RPCAPI supports the create_port method."""
return self.client.can_send_version("1.41")
return self._can_send_version("1.41")
def can_send_rescue(self):
"""Return whether the RPCAPI supports node rescue methods."""
return self.client.can_send_version("1.43")
return self._can_send_version("1.43")
def create_node(self, context, node_obj, topic=None):
"""Synchronously, have a conductor validate and create a node.
@ -1047,16 +1060,16 @@ class ConductorAPI(object):
"""
new_kws = {}
version = '1.34'
if self.client.can_send_version('1.42'):
if self._can_send_version('1.42'):
version = '1.42'
new_kws['agent_version'] = agent_version
if self.client.can_send_version('1.49'):
if self._can_send_version('1.49'):
version = '1.49'
new_kws['agent_token'] = agent_token
if self.client.can_send_version('1.51'):
if self._can_send_version('1.51'):
version = '1.51'
new_kws['agent_verify_ca'] = agent_verify_ca
if self.client.can_send_version('1.54'):
if self._can_send_version('1.54'):
version = '1.54'
new_kws['agent_status'] = agent_status
new_kws['agent_status_message'] = agent_status_message
@ -1082,7 +1095,7 @@ class ConductorAPI(object):
:returns: The result of the action method, which may (or may not)
be an instance of the implementing VersionedObject class.
"""
if not self.client.can_send_version('1.31'):
if not self._can_send_version('1.31'):
raise NotImplementedError(_('Incompatible conductor version - '
'please upgrade ironic-conductor '
'first'))
@ -1108,7 +1121,7 @@ class ConductorAPI(object):
:returns: A tuple with the updates made to the object and
the result of the action method
"""
if not self.client.can_send_version('1.31'):
if not self._can_send_version('1.31'):
raise NotImplementedError(_('Incompatible conductor version - '
'please upgrade ironic-conductor '
'first'))
@ -1133,7 +1146,7 @@ class ConductorAPI(object):
upgrade
:returns: The downgraded instance of objinst
"""
if not self.client.can_send_version('1.31'):
if not self._can_send_version('1.31'):
raise NotImplementedError(_('Incompatible conductor version - '
'please upgrade ironic-conductor '
'first'))

View File

@ -362,7 +362,8 @@ service_opts = [
cfg.StrOpt('rpc_transport',
default='oslo',
choices=[('oslo', _('use oslo.messaging transport')),
('json-rpc', _('use JSON RPC transport'))],
('json-rpc', _('use JSON RPC transport')),
('none', _('No RPC, only use local conductor'))],
help=_('Which RPC transport implementation to use between '
'conductor and API services')),
cfg.BoolOpt('minimum_memory_warning_only',

View File

@ -55,3 +55,25 @@ class TestRPCService(base.TestCase):
mock_init_method.assert_called_once_with(self.rpc_svc.manager,
mock_ctx.return_value)
self.assertIs(rpc.GLOBAL_MANAGER, self.rpc_svc.manager)
@mock.patch.object(manager.ConductorManager, 'prepare_host', autospec=True)
@mock.patch.object(oslo_messaging, 'Target', autospec=True)
@mock.patch.object(objects_base, 'IronicObjectSerializer', autospec=True)
@mock.patch.object(rpc, 'get_server', autospec=True)
@mock.patch.object(manager.ConductorManager, 'init_host', autospec=True)
@mock.patch.object(context, 'get_admin_context', autospec=True)
def test_start_no_rpc(self, mock_ctx, mock_init_method,
mock_rpc, mock_ios, mock_target,
mock_prepare_method):
CONF.set_override('rpc_transport', 'none')
self.rpc_svc.start()
self.assertIsNone(self.rpc_svc.rpcserver)
mock_ctx.assert_called_once_with()
mock_target.assert_not_called()
mock_rpc.assert_not_called()
mock_ios.assert_called_once_with(is_server=True)
mock_prepare_method.assert_called_once_with(self.rpc_svc.manager)
mock_init_method.assert_called_once_with(self.rpc_svc.manager,
mock_ctx.return_value)
self.assertIs(rpc.GLOBAL_MANAGER, self.rpc_svc.manager)

View File

@ -77,6 +77,12 @@ class RPCAPITestCase(db_base.DbTestCase):
self.context, objects.Node(), self.fake_node)
self.fake_portgroup = db_utils.get_test_portgroup()
def test_rpc_disabled(self):
CONF.set_override('rpc_transport', 'none')
rpcapi = conductor_rpcapi.ConductorAPI(topic='fake-topic')
self.assertIsNone(rpcapi.client)
self.assertTrue(rpcapi._can_send_version('9.99'))
def test_serialized_instance_has_uuid(self):
self.assertIn('uuid', self.fake_node)
@ -726,6 +732,17 @@ class RPCAPITestCase(db_base.DbTestCase):
mock_manager.create_node.assert_called_once_with(
mock.sentinel.context, node_obj=mock.sentinel.node)
@mock.patch.object(rpc, 'GLOBAL_MANAGER',
spec_set=conductor_manager.ConductorManager)
def test_local_call_with_rpc_disabled(self, mock_manager):
CONF.set_override('host', 'fake.host')
CONF.set_override('rpc_transport', 'none')
rpcapi = conductor_rpcapi.ConductorAPI(topic='fake.topic')
rpcapi.create_node(mock.sentinel.context, mock.sentinel.node,
topic='fake.topic.fake.host')
mock_manager.create_node.assert_called_once_with(
mock.sentinel.context, node_obj=mock.sentinel.node)
@mock.patch.object(rpc, 'GLOBAL_MANAGER',
spec_set=conductor_manager.ConductorManager)
def test_local_call_host_mismatch(self, mock_manager):
@ -738,6 +755,27 @@ class RPCAPITestCase(db_base.DbTestCase):
rpcapi.client.prepare.assert_called_once_with(
topic='fake.topic.not-fake.host', version=mock.ANY)
@mock.patch.object(rpc, 'GLOBAL_MANAGER',
spec_set=conductor_manager.ConductorManager)
def test_local_call_host_mismatch_with_rpc_disabled(self, mock_manager):
CONF.set_override('host', 'fake.host')
CONF.set_override('rpc_transport', 'none')
rpcapi = conductor_rpcapi.ConductorAPI(topic='fake.topic')
self.assertRaises(exception.ServiceUnavailable,
rpcapi.create_node,
mock.sentinel.context, mock.sentinel.node,
topic='fake.topic.not-fake.host')
@mock.patch.object(rpc, 'GLOBAL_MANAGER', None)
def test_local_call_no_conductor_with_rpc_disabled(self):
CONF.set_override('host', 'fake.host')
CONF.set_override('rpc_transport', 'none')
rpcapi = conductor_rpcapi.ConductorAPI(topic='fake.topic')
self.assertRaises(exception.ServiceUnavailable,
rpcapi.create_node,
mock.sentinel.context, mock.sentinel.node,
topic='fake.topic.fake.host')
@mock.patch.object(rpc, 'GLOBAL_MANAGER',
spec_set=conductor_manager.ConductorManager)
def test_local_cast(self, mock_manager):

View File

@ -0,0 +1,5 @@
---
features:
- |
Adds a new ``none`` RPC transport that can be used together with the
combined ``ironic`` executable to completely disable the RPC bus.