This blueprint aims to enhance the existing usage of oslo.messaging to make the version scheme more clear to developers. Several people have expressed that the current code is difficult to understand, so this spec aims to improve that.
Neutron uses the oslo.messaging library to provide an internal communication channel between Neutron services. This communication is typically done via AMQP, but those details are mostly hidden by the use of oslo.messaging and it could be some other protocol in the future.
RPC APIs are defined in Neutron in two parts: client side and server side.
Here is an example of an rpc client definition:
from oslo import messaging from neutron.common import rpc as n_rpc class ClientAPI(object): """Client side RPC interface definition. API version history: 1.0 - Initial version 1.1 - Added my_remote_method_2 """ def __init__(self, topic): target = messaging.Target(topic=topic, version='1.0') self.client = n_rpc.get_client(target) def my_remote_method(self, context, arg1, arg2): cctxt = self.client.prepare() return cctxt.call(context, 'my_remote_method', arg1=arg1, arg2=arg2) def my_remote_method_2(self, context, arg1): cctxt = self.client.prepare(version='1.1') return cctxt.call(context, 'my_remote_method_2', arg1=arg1)
This class defines the client side interface for an rpc API. The interface has 2 methods. The first method existed in version 1.0 of the interface. The second method was added in version 1.1. When the newer method is called, it specifies that the remote side must implement at least version 1.1 to handle this request.
The server side of an rpc interface looks like this:
from oslo import messaging class ServerAPI(object): target = messaging.Target(version='1.1') def my_remote_method(self, context, arg1, arg2): return 'foo' def my_remote_method_2(self, context, arg1): return 'bar'
This class implements the server side of the interface. The messaging.Target() defined says that this class currently implements version 1.1 of the interface.
Note that changes to rpc interfaces must always be done in a backwards compatible way. The server side should always be able to handle older clients (within the same major version series, such as 1.X).
It is possible to bump the major version number and drop some code only needed for backwards compatibility. For more information about how to do that, see https://wiki.openstack.org/wiki/RpcMajorVersionUpdates.
Each stream of version numbers is associated with a target. On the client side, you set up a target that identifies the destination of your method call. Every method exposed at that target is a single API from the oslo.messaging perspective, regardless of the implementation details on the other side.
Unfortunately, in the Neutron code today, it’s inconsistent and generally difficult to tell what is considered a part of the same version stream or is separate.
There is an RPC interface defined that allows the Neutron plugin to remotely invoke methods in the DHCP agent. The client side is defined in neutron.api.rpc.agentnotifiers.dhcp_rpc_agent_api.DhcpAgentNotifyApi. The server side of this interface that runs in the DHCP agent is neutron.agent.dhcp_agent.DhcpAgent.
The DHCP agent includes a client API, neutron.agent.dhcp_agent.DhcpPluginAPI. The DHCP agent uses this class to call remote methods back in the Neutron server. The server side is defined in neutron.api.rpc.handlers.dhcp_rpc.DhcpRpcCallback. This is where versioning starts to get complicated.
A version number is associated with an oslo.messaging.Target. The target of a message is specified using a topic (and optionally a specific host on that topic). Everything exposed via the same target must be versioned as a part of the same version stream. The DhcpRpcCallback API is exposed on the ‘q-plugin’ topic. Several other things are exposed on the same topic. For example, when using the ml2 plugin, the relevant code is:
self.endpoints = [rpc.RpcCallbacks(self.notifier, self.type_manager), securitygroups_rpc.SecurityGroupServerRpcCallback(), dvr_rpc.DVRServerRpcCallback(), dhcp_rpc.DhcpRpcCallback(), agents_db.AgentExtRpcCallback(), metadata_rpc.MetadataRpcCallback()]
With the way the code is written today, the API exposed via the ‘q-plugin’ topic is the union of the APIs defined by the following classes:
neutron.plugins.ml2.rpc.RpcCallbacks neutron.plugins.ml2.drivers.type_tunnel.TunnelRpcCallbackMixin neutron.api.rpc.handlers.securitygroups_rpc.SecurityGroupServerRpcCallback neutron.api.rpc.handlers.dvr_rpc.DVRServerRpcCallback neutron.api.rpc.handlers.dhcp_rpc.DhcpRpcCallback neutron.db.agents_db.AgentExtRpcCallback neutron.api.rpc.handlers.metadata_rpc.MetadataRpcCallback
That means that an API change to ANY of these APIs means an increase in a version number stream that spans all of these interfaces. However, the implementation seems to suggest that they are versioned independently, but it’s not consistent.
This spec proposes two changes. The goal of the changes is to help make it more clear what the scope of the version numbers are. It should make handling of these version numbers a bit easier to understand and hopefully less error prone as a result.
For every client and server rpc interface definition in Neutron, add documentation that makes it very clear where the corresponding other side of the interface lives in the code. Also, the documentation will list any other classes that are included in the same version stream.
When defining an oslo.messaging.Target, you can add a namespace. This allows you to expose multiple interfaces on the same topic but still version them independently.
If we revisit the example of the ‘q-plugin’ topic and the ml2 driver, we have several interfaces exposed on the same topic in the default namespace. Namespaces will be used to separate these interfaces so that each interface can be versioned independently.
For example, one of the interfaces exposed is neutron.api.rpc.handlers.dhcp_rpc.DhcpRpcCallback. The beginning of that implementation looks like this:
class DhcpRpcCallback(object): """DHCP agent RPC callback in plugin implementations.""" target = messaging.Target(version='1.1')
Instead, the target will now be defined with a namespace:
class DhcpRpcCallback(object): """DHCP agent RPC callback in plugin implementations.""" target = messaging.Target(namespace='dhcp', version='1.1')
Similarly, the client side must be updated as well. neutron.agent.dhcp_agent.DhcpPluginAPI.__init__ includes the following code:
target = messaging.Target(topic=topic, version='1.0') self.client = n_rpc.get_client(target)
The change here is quite similar to what’s done on the server side:
target = messaging.Target(namespace='dhcp', topic=topic, version='1.0') self.client = n_rpc.get_client(target)
Now, when the client makes a request it is being much more explicit. The server side will only look for the requested method on the endpoint marked with the ‘dhcp’ namespce, instead of all included endpoints.
Technically, this change is not backwards compatible. The reality is that only matters if you expect any sort of live upgrade to work. Since live upgrades aren’t actually expected to work, this spec proposes just accepting this theoretical incompatibility since it doesn’t matter in practice, yet.
The developer impact should be minimal. Hopefully the impact is just that when developers come across the need to change one of these interfaces, it’s a bit more clear what other parts of the code are impacted. Developers will also have to be aware of the new namespace usage for any new interfaces added in the future.
The only things proposed here are documentation and use of a standard oslo.messaging APIs.
Instead of using namespaces, you could expose each interface on its own topic. Namespaces are a bit more convenient, as a service tends to have one topic set up using common code. That common code would have to be reworked to support an arbitrary list of endpoints mapped to topics.
Existing testing will validate that these changes have not caused regressions.
Additional developer documentation is a key deliverable of this blueprint.