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 username = myName
password = myPassword 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 Using CLI
--------- ---------

View File

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

View File

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

View File

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

View File

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

View File

@ -69,3 +69,11 @@ def prepare_service(name, argv=None, conf=CONF):
def process_launcher(): def process_launcher():
return service.ProcessLauncher(CONF, restart_method='mutate') 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, self.client = json_rpc.Client(serializer=serializer,
version_cap=version_cap) version_cap=version_cap)
self.topic = '' self.topic = ''
else: elif CONF.rpc_transport != 'none':
target = messaging.Target(topic=self.topic, version='1.0') target = messaging.Target(topic=self.topic, version='1.0')
self.client = rpc.get_client(target, version_cap=version_cap, self.client = rpc.get_client(target, version_cap=version_cap,
serializer=serializer) serializer=serializer)
else:
self.client = None
# NOTE(tenbrae): this is going to be buggy # NOTE(tenbrae): this is going to be buggy
self.ring_manager = hash_ring.HashRingManager() self.ring_manager = hash_ring.HashRingManager()
@ -203,6 +205,13 @@ class ConductorAPI(object):
# conductor. # conductor.
return _LOCAL_CONTEXT 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 # Normal RPC path
return self.client.prepare(topic=topic, version=version) return self.client.prepare(topic=topic, version=version)
@ -276,13 +285,17 @@ class ConductorAPI(object):
"""Get RPC topic name for the current conductor.""" """Get RPC topic name for the current conductor."""
return self.topic + "." + CONF.host 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): def can_send_create_port(self):
"""Return whether the RPCAPI supports the create_port method.""" """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): def can_send_rescue(self):
"""Return whether the RPCAPI supports node rescue methods.""" """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): def create_node(self, context, node_obj, topic=None):
"""Synchronously, have a conductor validate and create a node. """Synchronously, have a conductor validate and create a node.
@ -1047,16 +1060,16 @@ class ConductorAPI(object):
""" """
new_kws = {} new_kws = {}
version = '1.34' version = '1.34'
if self.client.can_send_version('1.42'): if self._can_send_version('1.42'):
version = '1.42' version = '1.42'
new_kws['agent_version'] = agent_version new_kws['agent_version'] = agent_version
if self.client.can_send_version('1.49'): if self._can_send_version('1.49'):
version = '1.49' version = '1.49'
new_kws['agent_token'] = agent_token new_kws['agent_token'] = agent_token
if self.client.can_send_version('1.51'): if self._can_send_version('1.51'):
version = '1.51' version = '1.51'
new_kws['agent_verify_ca'] = agent_verify_ca 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' version = '1.54'
new_kws['agent_status'] = agent_status new_kws['agent_status'] = agent_status
new_kws['agent_status_message'] = agent_status_message 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) :returns: The result of the action method, which may (or may not)
be an instance of the implementing VersionedObject class. 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 - ' raise NotImplementedError(_('Incompatible conductor version - '
'please upgrade ironic-conductor ' 'please upgrade ironic-conductor '
'first')) 'first'))
@ -1108,7 +1121,7 @@ class ConductorAPI(object):
:returns: A tuple with the updates made to the object and :returns: A tuple with the updates made to the object and
the result of the action method 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 - ' raise NotImplementedError(_('Incompatible conductor version - '
'please upgrade ironic-conductor ' 'please upgrade ironic-conductor '
'first')) 'first'))
@ -1133,7 +1146,7 @@ class ConductorAPI(object):
upgrade upgrade
:returns: The downgraded instance of objinst :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 - ' raise NotImplementedError(_('Incompatible conductor version - '
'please upgrade ironic-conductor ' 'please upgrade ironic-conductor '
'first')) 'first'))

View File

@ -362,7 +362,8 @@ service_opts = [
cfg.StrOpt('rpc_transport', cfg.StrOpt('rpc_transport',
default='oslo', default='oslo',
choices=[('oslo', _('use oslo.messaging transport')), 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 ' help=_('Which RPC transport implementation to use between '
'conductor and API services')), 'conductor and API services')),
cfg.BoolOpt('minimum_memory_warning_only', 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_init_method.assert_called_once_with(self.rpc_svc.manager,
mock_ctx.return_value) mock_ctx.return_value)
self.assertIs(rpc.GLOBAL_MANAGER, self.rpc_svc.manager) 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.context, objects.Node(), self.fake_node)
self.fake_portgroup = db_utils.get_test_portgroup() 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): def test_serialized_instance_has_uuid(self):
self.assertIn('uuid', self.fake_node) self.assertIn('uuid', self.fake_node)
@ -726,6 +732,17 @@ class RPCAPITestCase(db_base.DbTestCase):
mock_manager.create_node.assert_called_once_with( mock_manager.create_node.assert_called_once_with(
mock.sentinel.context, node_obj=mock.sentinel.node) 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', @mock.patch.object(rpc, 'GLOBAL_MANAGER',
spec_set=conductor_manager.ConductorManager) spec_set=conductor_manager.ConductorManager)
def test_local_call_host_mismatch(self, mock_manager): 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( rpcapi.client.prepare.assert_called_once_with(
topic='fake.topic.not-fake.host', version=mock.ANY) 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', @mock.patch.object(rpc, 'GLOBAL_MANAGER',
spec_set=conductor_manager.ConductorManager) spec_set=conductor_manager.ConductorManager)
def test_local_cast(self, mock_manager): 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.