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:
parent
9a6f2d101b
commit
019ed2d7b1
@ -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
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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',
|
||||||
|
@ -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:
|
||||||
|
@ -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.")
|
||||||
|
@ -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'))
|
||||||
|
@ -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',
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
5
releasenotes/notes/rpc-none-f05dac657eef4b66.yaml
Normal file
5
releasenotes/notes/rpc-none-f05dac657eef4b66.yaml
Normal 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.
|
Loading…
Reference in New Issue
Block a user