RSA Remote Services using XmlRpc transport¶
Authors: | Scott Lewis, Thomas Calmant |
---|
Introduction¶
This tutorial shows how to create and run a simple remote service using the XmlRpc provider. The XmlRpc distribution provider is one of several supported by the RSA Remote Services (OSGi R7-compliant) implementation.
Requirements¶
This tutorial sample requires Python 3.4+ or Python 2.7, and version 0.8.0+ of iPOPO.
Defining the Remote Service with a Python Class¶
We’ll start by defining a Python hello service that can to be exported by RSA for remote access.
In the sample.rsa
package is the helloimpl_xmlrpc
module, containing the
XmlRpcHelloImpl
class
@ComponentFactory("helloimpl-xmlrpc-factory")
@Provides(
"org.eclipse.ecf.examples.hello.IHello"
)
@Instantiate(
"helloimpl-xmlrpc",
{
# uncomment to automatically export upon creation
# "service.exported.interfaces":"*",
"osgi.basic.timeout": 60000,
},
)
class XmlRpcHelloImpl(HelloImpl):
pass
The XmlRpcHelloImpl
class has no body/implementation as it inherits its
implementation from the HelloImpl
class, which we will discuss in a moment.
The important parts of this class declaration for remote services are the
@Provides
class decorator and the commented-out
service.exported.interfaces and osgi.basic.timeout properties in the
@Instantiate
decorator.
The @Provides
class decorator gives the name of the service
specification provided by this instance.
This is the name that both local and remote consumers use to lookup this
service, even if it’s local-only (i.e. not a remote service).
In this case, since the original IHello
interface is a java interface class,
the fully-qualified name of the
interface class is used.
For an example of Java↔Python remote services see
this tutorial.
For Python-only remote services it’s not really necessary for this service specification be the name of a Java class, any unique String could have been used.
The osgi.basic.timeout is an optional property that gives a maximum time (in milliseconds) that the consumer will wait for a response before timing out.
The service.exported.interfaces property is a
required property for remote service export.
If one wants to have a remote service exported immediately upon instantiation
and registration as an iPOPO service, this property can be set to value *
which means to export all service interfaces.
The service.exported.interfaces property is commented out so that it is not exported immediately upon instantiation and registration. Instead, for this tutorial the export is performed via iPOPO console commands. If these comments were to be removed, the RSA impl will export this service as soon as it is instantiated and registered, making it unnecessary to explicitly export the service as shown in Exporting the XmlRpcHelloImpl as a Remote Service section below.
The HelloImpl Implementation¶
The XmlRpcHelloImpl
class delegates all the actual implementation to the
HelloImpl
class, which has the code for the methods defined for the
“org.eclipse.ecf.examples.hello.IHello” service specification name, with the
main method sayHello
:
class HelloImpl(object):
def sayHello(self, name='Not given', message='nothing'):
print(
"Python.sayHello called by: {0} with message: '{1}'".format(
name, message))
return "PythonSync says: Howdy {0} that's a nice runtime you got there".format(
name)
The sayHello
method is invoked via a remote service consumer once the
service has been exporting.
Exporting the XmlRpcHelloImpl as a Remote Service¶
Go to the pelix home directory and start the run_rsa_xmlrpc.py
main program
ipopo-1.0.0$ python -m samples.run_rsa_xmlrpc
** Pelix Shell prompt **
$
To load the module and instantiate and register an XmlRpcHelloImpl
instance
type
$ start samples.rsa.helloimpl_xmlrpc
Bundle ID: 18
Starting bundle 18 (samples.rsa.helloimpl_xmlrpc)...
In your environment, bundle number might not be 18… that is fine.
If you list services using the sl
console command you should see an instance
of IHello
service
$ sl org.eclipse.ecf.examples.hello.IHello
+----+-------------------------------------------+--------------------------------------------------+---------+
| ID | Specifications | Bundle | Ranking |
+====+===========================================+==================================================+=========+
| 20 | ['org.eclipse.ecf.examples.hello.IHello'] | Bundle(ID=18, Name=samples.rsa.helloimpl_xmlrpc) | 0 |
+----+-------------------------------------------+--------------------------------------------------+---------+
1 services registered
The service ID (20 in this case) may not be the same in your environment… again that is ok… but make a note of what the service ID is.
To export this service instance as remote service and make it available for
remote access, use the exportservice
command in the pelix console,
giving the number (20 from above) of the service to export:
$ exportservice 20 # use the service id for the org.eclipse.ecf.examples.hello.IHello service if not 20
Service=ServiceReference(ID=20, Bundle=18, Specs=['org.eclipse.ecf.examples.hello.IHello']) exported by 1 providers. EDEF written to file=edef.xml
$
This means that the service has been successfully exported.
To see this use the listexports
console command:
$ listexports
+--------------------------------------+-------------------------------+------------+
| Endpoint ID | Container ID | Service ID |
+======================================+===============================+============+
| b96927ad-1d00-45ad-848a-716d6cde8443 | http://127.0.0.1:8181/xml-rpc | 20 |
+--------------------------------------+-------------------------------+------------+
$ listexports b96927ad-1d00-45ad-848a-716d6cde8443
Endpoint description for endpoint.id=b96927ad-1d00-45ad-848a-716d6cde8443:
<?xml version='1.0' encoding='cp1252'?>
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
<endpoint-description>
<property name="objectClass" value-type="String">
<array>
<value>org.eclipse.ecf.examples.hello.IHello</value>
</array>
</property>
<property name="remote.configs.supported" value-type="String">
<array>
<value>ecf.xmlrpc.server</value>
</array>
</property>
<property name="service.imported.configs" value-type="String">
<array>
<value>ecf.xmlrpc.server</value>
</array>
</property>
<property name="remote.intents.supported" value-type="String">
<array>
<value>osgi.basic</value>
<value>osgi.async</value>
</array>
</property>
<property name="service.intents" value-type="String">
<array>
<value>osgi.async</value>
</array>
</property>
<property name="endpoint.service.id" value="20" value-type="Long">
</property>
<property name="service.id" value="20" value-type="Long">
</property>
<property name="endpoint.framework.uuid" value="4d541077-ee2a-4d68-85f5-be529f89bec0" value-type="String">
</property>
<property name="endpoint.id" value="b96927ad-1d00-45ad-848a-716d6cde8443" value-type="String">
</property>
<property name="service.imported" value="true" value-type="String">
</property>
<property name="ecf.endpoint.id" value="http://127.0.0.1:8181/xml-rpc" value-type="String">
</property>
<property name="ecf.endpoint.id.ns" value="ecf.namespace.xmlrpc" value-type="String">
</property>
<property name="ecf.rsvc.id" value="3" value-type="Long">
</property>
<property name="ecf.endpoint.ts" value="1534119904514" value-type="Long">
</property>
<property name="osgi.basic.timeout" value="60000" value-type="Long">
</property>
</endpoint-description>
</endpoint-descriptions>
$
Note that listexports
produced a small table with Endpoint ID,
Container ID, and Service ID columns.
As shown above, if the Endpoint ID is copied and used in listexports, it will
then print out the endpoint description (XML) for the newly-created endpoint.
Also as indicated in the exportservice
command output, a file edef.xml
has also been written to the filesystem containing the endpoint description XML
known as EDEF).
EDEF is a standardized XML format
that gives all of the remote service meta-data required for a consumer to import
an endpoint.
The edef.xml file will contain the same XML printed to the console via the
listexports b96927ad-1d00-45ad-848a-716d6cde8443
console command.
Importing the XmlRpcHelloImpl Remote Service¶
For a consumer to use this remote service, another python process should be started using the same command:
ipopo-1.0.0$ python -m samples.run_rsa_xmlrpc
** Pelix Shell prompt **
$
If you have started this second python process from the same location,
all that’s necessary to trigger the import of the remote service, and have a
consumer sample start to call it’s methods is to use the importservice
console command:
$ importservice
Imported 1 endpoints from EDEF file=edef.xml
Python IHello service consumer received sync response: PythonSync says: Howdy PythonSync that's a nice runtime you got there
done with sayHelloAsync method
done with sayHelloPromise method
Proxy service=ServiceReference(ID=21, Bundle=7, Specs=['org.eclipse.ecf.examples.hello.IHello']) imported. rsid=http://127.0.0.1:8181/xml-rpc:3
$ async response: PythonAsync says: Howdy PythonAsync that's a nice runtime you got there
promise response: PythonPromise says: Howdy PythonPromise that's a nice runtime you got there
This indicates that the remote service was imported, and the methods on the remote service were called by the consumer.
Here is the code for the consumer (also in
samples/rsa/helloconsumer_xmlrpc.py
)
from pelix.ipopo.decorators import ComponentFactory, Instantiate, Requires, Validate
from concurrent.futures import ThreadPoolExecutor
@ComponentFactory("remote-hello-consumer-factory")
# The '(service.imported=*)' filter only allows remote services to be injected
@Requires("_helloservice", "org.eclipse.ecf.examples.hello.IHello",
False, False, "(service.imported=*)", False)
@Instantiate("remote-hello-consumer")
class RemoteHelloConsumer(object):
def __init__(self):
self._helloservice = None
self._name = 'Python'
self._msg = 'Hello Java'
self._executor = ThreadPoolExecutor()
@Validate
def _validate(self, bundle_context):
# call it!
resp = self._helloservice.sayHello(self._name + 'Sync', self._msg)
print(
"{0} IHello service consumer received sync response: {1}".format(
self._name,
resp))
# call sayHelloAsync which returns Future and we add lambda to print
# the result when done
self._executor.submit(
self._helloservice.sayHelloAsync,
self._name + 'Async',
self._msg).add_done_callback(
lambda f: print(
'async response: {0}'.format(
f.result())))
print("done with sayHelloAsync method")
# call sayHelloAsync which returns Future and we add lambda to print
# the result when done
self._executor.submit(
self._helloservice.sayHelloPromise,
self._name + 'Promise',
self._msg).add_done_callback(
lambda f: print(
'promise response: {0}'.format(
f.result())))
print("done with sayHelloPromise method")
For having this remote service injected, the important part of things is the
@Requires
decorator
@Requires("_helloservice", "org.eclipse.ecf.examples.hello.IHello",
False, False, "(service.imported=*)", False)
This gives the specification name required org.eclipse.ecf.examples.hello.IHello, and it also gives an OSGi filter
"(service.imported=*)"
As per the Remote Service spec
this requires that the IHello
service is a remote service, as all proxies
must have the service.imported property set, indicating that it was
imported.
When importservice
is executed the RSA implementation does the following:
- Reads the edef.xml from filesystem (i.e. ‘discovers the service’)
- Create a local proxy for the remote service using the edef.xml file
- The proxy is injected by iPOPO into the
RemoteHelloConsumer._helloservice
member - The
_activated
method is called by iPOPO, which uses theself._helloservice
proxy to send the method calls to the remote service, using HTTP and XML-RPC to serialize thesayHello
method arguments, send the request via HTTP, get the return value back, and print the return value to the consumer’s console.
Note that with Export, rather than using the console’s exportservice
command, it may be invoked programmatically, or automatically by the topology
manager (for example upon service registration).
For Import, the importservice
command may also be invoked automatically,
or via remote service discovery (e.g. etcd, zookeeper, zeroconf, custom, etc).
The use of the console commands in this example was to demonstrate the dynamics
and flexibility provided by the OSGi R7-compliant RSA implementation.
Exporting Automatically upon Service Registration¶
To export automatically upon service registration, all that need be done is to
un-comment the setting the service.exported.interfaces property in the
Instantiate
decorator:
@ComponentFactory("helloimpl-xmlrpc-factory")
@Provides(
"org.eclipse.ecf.examples.hello.IHello"
)
@Instantiate(
"helloimpl-xmlrpc",
{
"service.exported.interfaces": "*",
"osgi.basic.timeout": 60000,
},
)
class XmlRpcHelloImpl(HelloImpl):
pass
Unlike in the example above, when this service is instantiated and registered,
it will also be automatically exported, making unnecessary to use the
exportservice
command.
Using Etcd Discovery¶
Rather than importing remote services manually via the importservice
command, it’s also possible to import using supported network discovery
protocols.
One discovery mechanism used in systems like
kubernetes is
etcd, and there is an etcd discovery
provider available in the pelix.rsa.providers.discovery.discovery_etcd
module.
This is the list of bundles included in the samples.run_rsa_etcd_xmlrpc
program:
bundles = ['pelix.ipopo.core',
'pelix.shell.core',
'pelix.shell.ipopo',
'pelix.shell.console',
'pelix.rsa.remoteserviceadmin', # RSA implementation
'pelix.http.basic', # httpservice
# xmlrpc distribution provider (opt)
'pelix.rsa.providers.distribution.xmlrpc',
# etcd discovery provider (opt)
'pelix.rsa.providers.discovery.discovery_etcd',
# basic topology manager (opt)
'pelix.rsa.topologymanagers.basic',
'pelix.rsa.shell', # RSA shell commands (opt)
'samples.rsa.helloconsumer_xmlrpc'] # Example helloconsumer. Only uses remote proxies
Note the presence of the etcd discovery provider:
pelix.rsa.providers.discovery.discovery_etcd
To start a consumer with etcd discovery run the samples.run_rsa_etcd_xmlrpc
program:
$ python -m samples.run_rsa_etcd_xmlrpc
** Pelix Shell prompt **
$ start samples.rsa.helloimpl_xmlrpc
Bundle ID: 19
Starting bundle 19 (samples.rsa.helloimpl_xmlrpc)...
$ sl org.eclipse.ecf.examples.hello.IHello
+----+-------------------------------------------+--------------------------------------------------+---------+
| ID | Specifications | Bundle | Ranking |
+====+===========================================+==================================================+=========+
| 21 | ['org.eclipse.ecf.examples.hello.IHello'] | Bundle(ID=19, Name=samples.rsa.helloimpl_xmlrpc) | 0 |
+----+-------------------------------------------+--------------------------------------------------+---------+
1 services registered
$ exportservice 21
Service=ServiceReference(ID=21, Bundle=19, Specs=['org.eclipse.ecf.examples.hello.IHello']) exported by 1 providers. EDEF written to file=edef.xml
$ lexps
+--------------------------------------+-------------------------------+------------+
| Endpoint ID | Container ID | Service ID |
+======================================+===============================+============+
| 0b5a6bf1-494e-41ef-861c-4c302ae75141 | http://127.0.0.1:8181/xml-rpc | 21 |
+--------------------------------------+-------------------------------+------------+
$
Then start a consumer process
$ python -m samples.run_rsa_etcd_xmlrpc
** Pelix Shell prompt **
$ Python IHello service consumer received sync response: PythonSync says: Howdy PythonSync that's a nice runtime you got there
done with sayHelloAsync method
done with sayHelloPromise method
async response: PythonAsync says: Howdy PythonAsync that's a nice runtime you got there
promise response: PythonPromise says: Howdy PythonPromise that's a nice runtime you got there
This consumer uses etcd to discover the IHello
remote service, a proxy is
created and injected into the consumer (using the same consumer code shown
above), and the consumer calls this proxy producing the text output above on
the consumer and this output on the remote service implementation:
$ Python.sayHello called by: PythonSync with message: 'Hello Java'
Python.sayHelloAsync called by: PythonAsync with message: 'Hello Java'
Python.sayHelloPromise called by: PythonPromise with message: 'Hello Java'
The consumer discovered the org.eclipse.ecf.examples.hello.IHello
service
published via etcd discovery, injected it into the consumer and the consumer
called the methods on the IHello
remote service, producing output on both
the consumer and the remote service implementation.