"""
A simple `JSON-RPC`_ client using `requests`_ for the http connection.
.. _JSON-RPC: http://www.jsonrpc.org/specification
.. _requests: http://docs.python-requests.org
The RPC protocol is built on http(s), with the body containing
a json-encoded dictionary:
Request Object:
* method (string) - Name of the method to be invoked.
* params (dict) - Keyword arguments to the method.
Response Object:
* result (object) - The returned result from the method. This is REQUIRED on
success, and MUST NOT exist if there was an error.
* error (object) - A description of the error, likely an Exception object.
This is REQUIRED on error and MUST NOT exist on success.
"""
import logging
import time
import random
from threading import RLock
import requests
from iceprod.core.jsonUtil import json_encode,json_decode
logger = logging.getLogger('jsonrpc')
[docs]class Client(object):
"""Raw JSONRPC client object"""
id = 0
idlock = RLock()
def __init__(self,timeout=60.0,address=None,backoff=True,**kwargs):
if address is None:
raise Exception('need a valid address')
self.__timeout = timeout
self.__address = address
self.__backoff = backoff
self.__kwargs = kwargs
self.open() # start session
[docs] def open(self):
"""Open the http session"""
logger.warning('establish http session for jsonrpc')
self.__session = requests.Session()
if 'username' in self.__kwargs and 'password' in self.__kwargs:
self.__session.auth = (self.__kwargs['username'], self.__kwargs['password'])
if 'sslcert' in self.__kwargs:
if 'sslkey' in self.__kwargs:
self.__session.cert = (self.__kwargs['sslcert'], self.__kwargs['sslkey'])
else:
self.__session.cert = self.__kwargs['sslcert']
if 'cacert' in self.__kwargs:
self.__session.verify = self.__kwargs['cacert']
[docs] def close(self):
"""Close the http session"""
logger.warning('close jsonrpc http session')
self.__session.close()
@classmethod
def newid(cls):
cls.idlock.acquire()
id = cls.id
cls.id += 1
cls.idlock.release()
return id
[docs] def request(self,methodname,kwargs):
"""Send request to RPC Server"""
# check method name for bad characters
if methodname[0] == '_':
logger.warning('cannot use RPC for private methods')
raise Exception('Cannot use RPC for private methods')
# translate request to json
body = json_encode({'jsonrpc': '2.0',
'method': methodname,
'params': kwargs,
'id': Client.newid(),
}).encode('utf-8')
# make request to server
data = None
for i in range(10):
try:
r = self.__session.post(self.__address, timeout=self.__timeout,
data=body, headers={'Content-Type': 'application/json-rpc'})
r.raise_for_status()
data = r.content
break
except Exception:
logger.warning('error making jsonrpc request for %s', methodname)
if self.__backoff and i < 2:
# try restarting connection, with backoff
self.close()
sleep_time = random.randint(i*2,(i+1)*30)
if isinstance(self.__backoff, (int,float)):
sleep_time *= self.__backoff
time.sleep(sleep_time)
self.open()
else:
raise
# translate response from json
if not data:
logger.warning('request returned empty string')
return None
try:
data = json_decode(data)
except Exception:
logger.info('json data: %r',data)
raise
if 'error' in data:
logger.warning('error: %r', data['error'])
try:
raise Exception('Error %r: %r %r'%data['error'])
except Exception:
raise Exception('Error %r'%data['error'])
if 'result' in data:
if isinstance(data['result'], Exception):
raise data['result']
return data['result']
else:
logger.info('result not in data: %r', data)
return None
[docs]class JSONRPC:
"""`JSON-RPC`_ client connection.
Call RPC functions as regular function calls.
Example::
rpc = JSONRPC('http://my.server/jsonrpc')
rpc.set_task_status(task_id,'waiting')
"""
def __init__(self, address=None, timeout=None, passkey=None, **kwargs):
"""Start the JSONRPC Client."""
self._address = address
self._timeout = timeout
self._passkey = passkey
self._rpc = None
self.start(**kwargs)
def start(self, **kwargs):
self._rpc = Client(timeout=self._timeout,
address=self._address,
**kwargs)
try:
ret = self._rpc.request('echo', {'value':'e', 'passkey':self._passkey})
except Exception as e:
logger.error('error',exc_info=True)
self.stop()
raise Exception('JSONRPC communcation did not start. '
'url=%s and passkey=%s'%(self._address,self._passkey))
if ret != 'e':
self.stop()
raise Exception('JSONRPC communication error when starting - '
'echo failed (%r). url=%s and passkey=%s'
%(ret,self._address,self._passkey))
[docs] def stop(self):
"""Stop the JSONRPC Client."""
self._rpc.close()
self._rpc = None
[docs] def restart(self):
"""Restart the JSONRPC Client."""
self._rpc.close()
self._rpc.open()
def __getattr__(self, name):
if self._rpc is None:
raise Exception('JSONRPC connection not started yet')
class _Method(object):
def __init__(self, rpc, passkey, name):
self.rpc = rpc
self.name = name
self.passkey = passkey
def __getattr__(self, name):
return _Method(self.rpc, self.passkey, "%s.%s"%(self.name,name))
def __call__(self, *args, **kwargs):
# add passkey to arguments
if 'passkey' not in kwargs:
kwargs['passkey'] = self.passkey
# jsonrpc can only handle args or kwargs, not both
# so turn args into kwargs
if len(args) > 0 and 'args' not in kwargs:
kwargs['args'] = args
return self.rpc.request(self.name,kwargs)
return _Method(self._rpc,self._passkey,name)