summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Tantsur <dtantsur@protonmail.com>2021-11-29 11:36:06 +0100
committerDmitry Tantsur <dtantsur@protonmail.com>2021-12-07 09:33:04 +0100
commit019ed2d7b11575326328231a7f3eca88240275ef (patch)
tree33b73fffdafc3e48144a72c7014dbca18d5a3987
parent9a6f2d101ba734126b3e7f0d475b747ebcb84b62 (diff)
downloadironic-019ed2d7b11575326328231a7f3eca88240275ef.tar.gz
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
-rw-r--r--doc/source/install/standalone/configure.rst14
-rw-r--r--ironic/api/wsgi.py1
-rw-r--r--ironic/cmd/api.py1
-rw-r--r--ironic/cmd/conductor.py1
-rw-r--r--ironic/common/rpc_service.py13
-rw-r--r--ironic/common/service.py8
-rw-r--r--ironic/conductor/rpcapi.py33
-rw-r--r--ironic/conf/default.py3
-rw-r--r--ironic/tests/unit/common/test_rpc_service.py22
-rw-r--r--ironic/tests/unit/conductor/test_rpcapi.py38
-rw-r--r--releasenotes/notes/rpc-none-f05dac657eef4b66.yaml5
11 files changed, 123 insertions, 16 deletions
diff --git a/doc/source/install/standalone/configure.rst b/doc/source/install/standalone/configure.rst
index 906e58b2c..fc540d986 100644
--- a/doc/source/install/standalone/configure.rst
+++ b/doc/source/install/standalone/configure.rst
@@ -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
---------
diff --git a/ironic/api/wsgi.py b/ironic/api/wsgi.py
index 467389701..bf98de90f 100644
--- a/ironic/api/wsgi.py
+++ b/ironic/api/wsgi.py
@@ -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)
diff --git a/ironic/cmd/api.py b/ironic/cmd/api.py
index 4a4b381c8..2323c4b09 100644
--- a/ironic/cmd/api.py
+++ b/ironic/cmd/api.py
@@ -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()
diff --git a/ironic/cmd/conductor.py b/ironic/cmd/conductor.py
index 19fb05cb4..843185890 100644
--- a/ironic/cmd/conductor.py
+++ b/ironic/cmd/conductor.py
@@ -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',
diff --git a/ironic/common/rpc_service.py b/ironic/common/rpc_service.py
index bbf38d7f4..78379c981 100644
--- a/ironic/common/rpc_service.py
+++ b/ironic/common/rpc_service.py
@@ -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:
diff --git a/ironic/common/service.py b/ironic/common/service.py
index db83c147a..c30df6f56 100644
--- a/ironic/common/service.py
+++ b/ironic/common/service.py
@@ -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.")
diff --git a/ironic/conductor/rpcapi.py b/ironic/conductor/rpcapi.py
index 6f4971be7..21139e60d 100644
--- a/ironic/conductor/rpcapi.py
+++ b/ironic/conductor/rpcapi.py
@@ -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'))
diff --git a/ironic/conf/default.py b/ironic/conf/default.py
index 1399b4f2e..3a6d3721d 100644
--- a/ironic/conf/default.py
+++ b/ironic/conf/default.py
@@ -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',
diff --git a/ironic/tests/unit/common/test_rpc_service.py b/ironic/tests/unit/common/test_rpc_service.py
index 4ba3b200a..4e190f5e6 100644
--- a/ironic/tests/unit/common/test_rpc_service.py
+++ b/ironic/tests/unit/common/test_rpc_service.py
@@ -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)
diff --git a/ironic/tests/unit/conductor/test_rpcapi.py b/ironic/tests/unit/conductor/test_rpcapi.py
index d207bb2f7..56f67b2c2 100644
--- a/ironic/tests/unit/conductor/test_rpcapi.py
+++ b/ironic/tests/unit/conductor/test_rpcapi.py
@@ -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)
@@ -728,6 +734,17 @@ class RPCAPITestCase(db_base.DbTestCase):
@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):
CONF.set_override('host', 'fake.host')
rpcapi = conductor_rpcapi.ConductorAPI(topic='fake.topic')
@@ -740,6 +757,27 @@ class RPCAPITestCase(db_base.DbTestCase):
@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):
CONF.set_override('host', 'fake.host')
rpcapi = conductor_rpcapi.ConductorAPI(topic='fake.topic')
diff --git a/releasenotes/notes/rpc-none-f05dac657eef4b66.yaml b/releasenotes/notes/rpc-none-f05dac657eef4b66.yaml
new file mode 100644
index 000000000..332ed241a
--- /dev/null
+++ b/releasenotes/notes/rpc-none-f05dac657eef4b66.yaml
@@ -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.