diff --git a/doc/source/install/standalone/configure.rst b/doc/source/install/standalone/configure.rst index 906e58b2c5..fc540d9864 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 4673897012..bf98de90fb 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 4a4b381c86..2323c4b09c 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 19fb05cb41..8431858906 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 bbf38d7f48..78379c9817 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 db83c147a9..c30df6f569 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 6f4971be71..21139e60dd 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 1399b4f2e2..3a6d3721da 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 4ba3b200ac..4e190f5e6f 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 d207bb2f7b..56f67b2c2e 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) @@ -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): diff --git a/releasenotes/notes/rpc-none-f05dac657eef4b66.yaml b/releasenotes/notes/rpc-none-f05dac657eef4b66.yaml new file mode 100644 index 0000000000..332ed241a7 --- /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.