Switch client to JSON-RPC

Modify ipalib.rpc to support JSON-RPC in addition to XML-RPC.
This is done by subclassing and extending xmlrpclib, because
our existing code relies on xmlrpclib internals.

The URI to use is given in the new jsonrpc_uri env variable. When
it is not given, it is generated from xmlrpc_uri by replacing
/xml with /json.

The rpc_json_uri env variable existed before, but was unused,
undocumented and not set the install scripts.
This patch removes it in favor of jsonrpc_uri (for consistency
with xmlrpc_uri).

Add the rpc_protocol env variable to control the protocol
IPA uses. rpc_protocol defaults to 'jsonrpc', but may be changed
to 'xmlrpc'.
Make backend.Executioner and tests use the backend specified by
rpc_protocol.

For compatibility with unwrap_xml, decoding JSON now gives tuples
instead of lists.

Design: http://freeipa.org/page/V3/JSON-RPC
Ticket: https://fedorahosted.org/freeipa/ticket/3299
This commit is contained in:
Petr Viktorin
2012-12-19 04:25:24 -05:00
parent a1165ffbb8
commit 1e836d2d0c
18 changed files with 329 additions and 176 deletions

View File

@@ -34,14 +34,14 @@ api.bootstrap_with_global_options(context='example')
api.finalize() api.finalize()
# You will need to create a connection. If you're in_server, call # You will need to create a connection. If you're in_server, call
# Backend.ldap.connect(), otherwise Backend.xmlclient.connect(). # Backend.ldap.connect(), otherwise Backend.rpcclient.connect().
if api.env.in_server: if api.env.in_server:
api.Backend.ldap2.connect( api.Backend.ldap2.connect(
ccache=api.Backend.krb.default_ccname() ccache=api.Backend.krb.default_ccname()
) )
else: else:
api.Backend.xmlclient.connect() api.Backend.rpcclient.connect()
# Now that you're connected, you can make calls to api.Command.whatever(): # Now that you're connected, you can make calls to api.Command.whatever():

View File

@@ -436,7 +436,7 @@ def main():
sys.exit("Failed to obtain host TGT.") sys.exit("Failed to obtain host TGT.")
# Now we have a TGT, connect to IPA # Now we have a TGT, connect to IPA
try: try:
api.Backend.xmlclient.connect() api.Backend.rpcclient.connect()
except errors.KerberosError, e: except errors.KerberosError, e:
sys.exit('Cannot connect to the server due to ' + str(e)) sys.exit('Cannot connect to the server due to ' + str(e))
try: try:

View File

@@ -1492,7 +1492,7 @@ def update_ssh_keys(server, hostname, ssh_dir, create_sshfp):
try: try:
# Use the RPC directly so older servers are supported # Use the RPC directly so older servers are supported
api.Backend.xmlclient.forward( api.Backend.rpcclient.forward(
'host_mod', 'host_mod',
unicode(hostname), unicode(hostname),
ipasshpubkey=[pk.openssh() for pk in pubkeys], ipasshpubkey=[pk.openssh() for pk in pubkeys],
@@ -2458,19 +2458,19 @@ def install(options, env, fstore, statestore):
# Now, let's try to connect to the server's XML-RPC interface # Now, let's try to connect to the server's XML-RPC interface
connected = False connected = False
try: try:
api.Backend.xmlclient.connect() api.Backend.rpcclient.connect()
connected = True connected = True
root_logger.debug('Try RPC connection') root_logger.debug('Try RPC connection')
api.Backend.xmlclient.forward('ping') api.Backend.rpcclient.forward('ping')
except errors.KerberosError, e: except errors.KerberosError, e:
if connected: if connected:
api.Backend.xmlclient.disconnect() api.Backend.rpcclient.disconnect()
root_logger.info('Cannot connect to the server due to ' + root_logger.info('Cannot connect to the server due to ' +
'Kerberos error: %s. Trying with delegate=True', str(e)) 'Kerberos error: %s. Trying with delegate=True', str(e))
try: try:
api.Backend.xmlclient.connect(delegate=True) api.Backend.rpcclient.connect(delegate=True)
root_logger.debug('Try RPC connection') root_logger.debug('Try RPC connection')
api.Backend.xmlclient.forward('ping') api.Backend.rpcclient.forward('ping')
root_logger.info('Connection with delegate=True successful') root_logger.info('Connection with delegate=True successful')
@@ -2493,7 +2493,7 @@ def install(options, env, fstore, statestore):
return CLIENT_INSTALL_ERROR return CLIENT_INSTALL_ERROR
# Use the RPC directly so older servers are supported # Use the RPC directly so older servers are supported
result = api.Backend.xmlclient.forward( result = api.Backend.rpcclient.forward(
'env', 'env',
server=True, server=True,
version=u'2.0', version=u'2.0',

View File

@@ -179,7 +179,13 @@ Used internally in the IPA source package to verify that the API has not changed
When True provides more information. Specifically this sets the global log level to "info". When True provides more information. Specifically this sets the global log level to "info".
.TP .TP
.B xmlrpc_uri <URI> .B xmlrpc_uri <URI>
Specifies the URI of the XML\-RPC server for a client. This is used by IPA and some external tools as well, such as ipa\-getcert. e.g. https://ipa.example.com/ipa/xml Specifies the URI of the XML\-RPC server for a client. This may be used by IPA, and is used by some external tools, such as ipa\-getcert. Example: https://ipa.example.com/ipa/xml
.TP
.B jsonrpc_uri <URI>
Specifies the URI of the JSON server for a client. This is used by IPA. If not given, it is derived from xmlrpc_uri. Example: https://ipa.example.com/ipa/json
.TP
.B rpc_protocol <URI>
Specifies the type of RPC calls IPA makes: 'jsonrpc' or 'xmlrpc'. Defaults to 'jsonrpc'.
.TP .TP
The following define the containers for the IPA server. Containers define where in the DIT that objects can be found. The full location is the value of container + basedn. The following define the containers for the IPA server. Containers define where in the DIT that objects can be found. The full location is the value of container + basedn.
container_accounts: cn=accounts container_accounts: cn=accounts

View File

@@ -113,7 +113,7 @@ class Executioner(Backend):
if self.env.in_server: if self.env.in_server:
self.Backend.ldap2.connect(ccache=ccache) self.Backend.ldap2.connect(ccache=ccache)
else: else:
self.Backend.xmlclient.connect(verbose=(self.env.verbose >= 2), self.Backend.rpcclient.connect(verbose=(self.env.verbose >= 2),
fallback=self.env.fallback, delegate=self.env.delegate) fallback=self.env.fallback, delegate=self.env.delegate)
if client_ip is not None: if client_ip is not None:
setattr(context, "client_ip", client_ip) setattr(context, "client_ip", client_ip)

View File

@@ -29,17 +29,17 @@ of the process.
For the per-request thread-local information, see `ipalib.request`. For the per-request thread-local information, see `ipalib.request`.
""" """
import urlparse
from ConfigParser import RawConfigParser, ParsingError from ConfigParser import RawConfigParser, ParsingError
from types import NoneType from types import NoneType
import os import os
from os import path from os import path
import sys import sys
from socket import getfqdn
from ipapython.dn import DN from ipapython.dn import DN
from base import check_name from base import check_name
from constants import CONFIG_SECTION from constants import CONFIG_SECTION
from constants import TYPE_ERROR, OVERRIDE_ERROR, SET_ERROR, DEL_ERROR from constants import OVERRIDE_ERROR, SET_ERROR, DEL_ERROR
class Env(object): class Env(object):
@@ -514,8 +514,8 @@ class Env(object):
``self.conf_default`` (if it exists) by calling ``self.conf_default`` (if it exists) by calling
`Env._merge_from_file()`. `Env._merge_from_file()`.
4. Intelligently fill-in the *in_server* , *logdir*, and *log* 4. Intelligently fill-in the *in_server* , *logdir*, *log*, and
variables if they haven't already been set. *jsonrpc_uri* variables if they haven't already been set.
5. Merge-in the variables in ``defaults`` by calling `Env._merge()`. 5. Merge-in the variables in ``defaults`` by calling `Env._merge()`.
In normal circumstances ``defaults`` will simply be those In normal circumstances ``defaults`` will simply be those
@@ -556,6 +556,19 @@ class Env(object):
if 'log' not in self: if 'log' not in self:
self.log = self._join('logdir', '%s.log' % self.context) self.log = self._join('logdir', '%s.log' % self.context)
# Derive jsonrpc_uri from xmlrpc_uri
if 'jsonrpc_uri' not in self:
if 'xmlrpc_uri' in self:
xmlrpc_uri = self.xmlrpc_uri
else:
xmlrpc_uri = defaults.get('xmlrpc_uri')
if xmlrpc_uri:
(scheme, netloc, uripath, params, query, fragment
) = urlparse.urlparse(xmlrpc_uri)
uripath = uripath.replace('/xml', '/json', 1)
self.jsonrpc_uri = urlparse.urlunparse((
scheme, netloc, uripath, params, query, fragment))
self._merge(**defaults) self._merge(**defaults)
def _finalize(self, **lastchance): def _finalize(self, **lastchance):

View File

@@ -111,10 +111,12 @@ DEFAULT_CONFIG = (
('container_otp', DN(('cn', 'otp'))), ('container_otp', DN(('cn', 'otp'))),
# Ports, hosts, and URIs: # Ports, hosts, and URIs:
# FIXME: let's renamed xmlrpc_uri to rpc_xml_uri
('xmlrpc_uri', 'http://localhost:8888/ipa/xml'), ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
('rpc_json_uri', 'http://localhost:8888/ipa/json'), # jsonrpc_uri is set in Env._finalize_core()
('ldap_uri', 'ldap://localhost:389'), ('ldap_uri', 'ldap://localhost:389'),
('rpc_protocol', 'jsonrpc'),
# Time to wait for a service to start, in seconds # Time to wait for a service to start, in seconds
('startup_timeout', 120), ('startup_timeout', 120),
@@ -199,5 +201,6 @@ DEFAULT_CONFIG = (
('in_server', object), # Whether or not running in-server (bool) ('in_server', object), # Whether or not running in-server (bool)
('logdir', object), # Directory containing log files ('logdir', object), # Directory containing log files
('log', object), # Path to context specific log file ('log', object), # Path to context specific log file
('jsonrpc_uri', object), # derived from xmlrpc_uri in Env._finalize_core()
) )

View File

@@ -742,7 +742,7 @@ class Command(HasParam):
actually work this command performs is executed locally. actually work this command performs is executed locally.
If running in a non-server context, `Command.forward` is called, If running in a non-server context, `Command.forward` is called,
which forwards this call over XML-RPC to the exact same command which forwards this call over RPC to the exact same command
on the nearest IPA server and the actual work this command on the nearest IPA server and the actual work this command
performs is executed remotely. performs is executed remotely.
""" """
@@ -777,9 +777,9 @@ class Command(HasParam):
def forward(self, *args, **kw): def forward(self, *args, **kw):
""" """
Forward call over XML-RPC to this same command on server. Forward call over RPC to this same command on server.
""" """
return self.Backend.xmlclient.forward(self.name, *args, **kw) return self.Backend.rpcclient.forward(self.name, *args, **kw)
def _on_finalize(self): def _on_finalize(self):
""" """

View File

@@ -1,8 +1,9 @@
# Authors: # Authors:
# Jason Gerard DeRose <jderose@redhat.com> # Jason Gerard DeRose <jderose@redhat.com>
# Rob Crittenden <rcritten@redhat.com> # Rob Crittenden <rcritten@redhat.com>
# Petr Viktorin <pviktori@redhat.com>
# #
# Copyright (C) 2008 Red Hat # Copyright (C) 2008-2013 Red Hat
# see file 'COPYING' for use and warranty information # see file 'COPYING' for use and warranty information
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
@@ -19,11 +20,31 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
XML-RPC client plugin. RPC client plugins.
""" """
from ipalib import api from ipalib import api
if 'in_server' in api.env and api.env.in_server is False: if 'in_server' in api.env and api.env.in_server is False:
from ipalib.rpc import xmlclient from ipalib.rpc import xmlclient, jsonclient
api.register(xmlclient) api.register(xmlclient)
api.register(jsonclient)
# FIXME: api.register only looks at the class name, so we need to create
# trivial subclasses with the desired name.
if api.env.rpc_protocol == 'xmlrpc':
class rpcclient(xmlclient):
"""xmlclient renamed to 'rpcclient'"""
pass
api.register(rpcclient)
elif api.env.rpc_protocol == 'jsonrpc':
class rpcclient(jsonclient):
"""jsonclient renamed to 'rpcclient'"""
pass
api.register(rpcclient)
else:
raise ValueError('unknown rpc_protocol: %s' % api.env.rpc_protocol)

View File

@@ -32,20 +32,25 @@ Also see the `ipaserver.rpcserver` module.
from types import NoneType from types import NoneType
from decimal import Decimal from decimal import Decimal
import threading
import sys import sys
import os import os
import errno
import locale import locale
import datetime import base64
import urllib
import json
import socket
from urllib2 import urlparse
from xmlrpclib import (Binary, Fault, dumps, loads, ServerProxy, Transport, from xmlrpclib import (Binary, Fault, dumps, loads, ServerProxy, Transport,
ProtocolError, MININT, MAXINT) ProtocolError, MININT, MAXINT)
import kerberos import kerberos
from dns import resolver, rdatatype from dns import resolver, rdatatype
from dns.exception import DNSException from dns.exception import DNSException
from nss.error import NSPRError
from ipalib.backend import Connectible from ipalib.backend import Connectible
from ipalib.errors import public_errors, PublicError, UnknownError, NetworkError, KerberosError, XMLRPCMarshallError from ipalib.errors import (public_errors, UnknownError, NetworkError,
KerberosError, XMLRPCMarshallError, JSONError, ConversionError)
from ipalib import errors from ipalib import errors
from ipalib.request import context, Connection from ipalib.request import context, Connection
from ipalib.util import get_current_principal from ipalib.util import get_current_principal
@@ -54,12 +59,7 @@ from ipapython import ipautil
from ipapython import kernel_keyring from ipapython import kernel_keyring
from ipapython.cookie import Cookie from ipapython.cookie import Cookie
from ipalib.text import _ from ipalib.text import _
import httplib
import socket
from ipapython.nsslib import NSSHTTPS, NSSConnection from ipapython.nsslib import NSSHTTPS, NSSConnection
from nss.error import NSPRError
from urllib2 import urlparse
from ipalib.krb_utils import KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, KRB5KRB_AP_ERR_TKT_EXPIRED, \ from ipalib.krb_utils import KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, KRB5KRB_AP_ERR_TKT_EXPIRED, \
KRB5_FCC_PERM, KRB5_FCC_NOFILE, KRB5_CC_FORMAT, KRB5_REALM_CANT_RESOLVE KRB5_FCC_PERM, KRB5_FCC_NOFILE, KRB5_CC_FORMAT, KRB5_REALM_CANT_RESOLVE
from ipapython.dn import DN from ipapython.dn import DN
@@ -67,6 +67,8 @@ from ipapython.dn import DN
COOKIE_NAME = 'ipa_session' COOKIE_NAME = 'ipa_session'
KEYRING_COOKIE_NAME = '%s_cookie:%%s' % COOKIE_NAME KEYRING_COOKIE_NAME = '%s_cookie:%%s' % COOKIE_NAME
errors_by_code = dict((e.errno, e) for e in public_errors)
def client_session_keyring_keyname(principal): def client_session_keyring_keyname(principal):
''' '''
@@ -228,6 +230,84 @@ def xml_dumps(params, methodname=None, methodresponse=False, encoding='UTF-8'):
) )
def json_encode_binary(val):
'''
JSON cannot encode binary values. We encode binary values in Python str
objects and text in Python unicode objects. In order to allow a binary
object to be passed through JSON we base64 encode it thus converting it to
text which JSON can transport. To assure we recognize the value is a base64
encoded representation of the original binary value and not confuse it with
other text we convert the binary value to a dict in this form:
{'__base64__' : base64_encoding_of_binary_value}
This modification of the original input value cannot be done "in place" as
one might first assume (e.g. replacing any binary items in a container
(e.g. list, tuple, dict) with the base64 dict because the container might be
an immutable object (i.e. a tuple). Therefore this function returns a copy
of any container objects it encounters with tuples replaced by lists. This
is O.K. because the JSON encoding will map both lists and tuples to JSON
arrays.
'''
if isinstance(val, dict):
new_dict = {}
for k, v in val.items():
new_dict[k] = json_encode_binary(v)
return new_dict
elif isinstance(val, (list, tuple)):
new_list = [json_encode_binary(v) for v in val]
return new_list
elif isinstance(val, str):
return {'__base64__': base64.b64encode(val)}
elif isinstance(val, Decimal):
return {'__base64__': base64.b64encode(str(val))}
elif isinstance(val, DN):
return str(val)
else:
return val
def json_decode_binary(val):
'''
JSON cannot transport binary data. In order to transport binary data we
convert binary data to a form like this:
{'__base64__' : base64_encoding_of_binary_value}
see json_encode_binary()
After JSON had decoded the JSON stream back into a Python object we must
recursively scan the object looking for any dicts which might represent
binary values and replace the dict containing the base64 encoding of the
binary value with the decoded binary value. Unlike the encoding problem
where the input might consist of immutable object, all JSON decoded
container are mutable so the conversion could be done in place. However we
don't modify objects in place because of side effects which may be
dangerous. Thus we elect to spend a few more cycles and avoid the
possibility of unintended side effects in favor of robustness.
'''
if isinstance(val, dict):
if '__base64__' in val:
return base64.b64decode(val['__base64__'])
else:
return dict((k, json_decode_binary(v)) for k, v in val.items())
elif isinstance(val, list):
return tuple(json_decode_binary(v) for v in val)
else:
if isinstance(val, basestring):
try:
return val.decode('utf-8')
except UnicodeDecodeError:
raise ConversionError(
name=val,
error='incorrect type'
)
else:
return val
def decode_fault(e, encoding='UTF-8'): def decode_fault(e, encoding='UTF-8'):
assert isinstance(e, Fault) assert isinstance(e, Fault)
if type(e.faultString) is str: if type(e.faultString) is str:
@@ -265,10 +345,48 @@ def xml_loads(data, encoding='UTF-8'):
raise decode_fault(e) raise decode_fault(e)
class LanguageAwareTransport(Transport): class DummyParser(object):
def __init__(self):
self.data = ''
def feed(self, data):
self.data += data
def close(self):
return self.data
class MultiProtocolTransport(Transport):
"""Transport that handles both XML-RPC and JSON"""
def __init__(self, protocol):
Transport.__init__(self)
self.protocol = protocol
def getparser(self):
if self.protocol == 'json':
parser = DummyParser()
return parser, parser
else:
return Transport.getparser(self)
def send_content(self, connection, request_body):
if self.protocol == 'json':
connection.putheader("Content-Type", "application/json")
else:
connection.putheader("Content-Type", "text/xml")
# gzip compression would be set up here, but we have it turned off
# (encode_threshold is None)
connection.putheader("Content-Length", str(len(request_body)))
connection.endheaders(request_body)
class LanguageAwareTransport(MultiProtocolTransport):
"""Transport sending Accept-Language header""" """Transport sending Accept-Language header"""
def get_host_info(self, host): def get_host_info(self, host):
(host, extra_headers, x509) = Transport.get_host_info(self, host) host, extra_headers, x509 = MultiProtocolTransport.get_host_info(
self, host)
try: try:
lang = locale.setlocale(locale.LC_ALL, '').split('.')[0].lower() lang = locale.setlocale(locale.LC_ALL, '').split('.')[0].lower()
@@ -468,23 +586,27 @@ class DelegatedKerbTransport(KerbTransport):
flags = kerberos.GSS_C_DELEG_FLAG | kerberos.GSS_C_MUTUAL_FLAG | \ flags = kerberos.GSS_C_DELEG_FLAG | kerberos.GSS_C_MUTUAL_FLAG | \
kerberos.GSS_C_SEQUENCE_FLAG kerberos.GSS_C_SEQUENCE_FLAG
class xmlclient(Connectible):
class RPCClient(Connectible):
""" """
Forwarding backend plugin for XML-RPC client. Forwarding backend plugin for XML-RPC client.
Also see the `ipaserver.rpcserver.xmlserver` plugin. Also see the `ipaserver.rpcserver.xmlserver` plugin.
""" """
def __init__(self): # Values to set on subclasses:
super(xmlclient, self).__init__() session_path = None
self.__errors = dict((e.errno, e) for e in public_errors) server_proxy_class = ServerProxy
protocol = None
env_rpc_uri_key = None
def get_url_list(self, xmlrpc_uri): def get_url_list(self, rpc_uri):
""" """
Create a list of urls consisting of the available IPA servers. Create a list of urls consisting of the available IPA servers.
""" """
# the configured URL defines what we use for the discovered servers # the configured URL defines what we use for the discovered servers
(scheme, netloc, path, params, query, fragment) = urlparse.urlparse(xmlrpc_uri) (scheme, netloc, path, params, query, fragment
) = urlparse.urlparse(rpc_uri)
servers = [] servers = []
name = '_ldap._tcp.%s.' % self.env.domain name = '_ldap._tcp.%s.' % self.env.domain
@@ -500,7 +622,7 @@ class xmlclient(Connectible):
servers = list(set(servers)) servers = list(set(servers))
# the list/set conversion won't preserve order so stick in the # the list/set conversion won't preserve order so stick in the
# local config file version here. # local config file version here.
cfg_server = xmlrpc_uri cfg_server = rpc_uri
if cfg_server in servers: if cfg_server in servers:
# make sure the configured master server is there just once and # make sure the configured master server is there just once and
# it is the first one # it is the first one
@@ -593,7 +715,7 @@ class xmlclient(Connectible):
# Form the session URL by substituting the session path into the original URL # Form the session URL by substituting the session path into the original URL
scheme, netloc, path, params, query, fragment = urlparse.urlparse(original_url) scheme, netloc, path, params, query, fragment = urlparse.urlparse(original_url)
path = '/ipa/session/xml' path = self.session_path
session_url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) session_url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
return session_url return session_url
@@ -601,31 +723,32 @@ class xmlclient(Connectible):
def create_connection(self, ccache=None, verbose=False, fallback=True, def create_connection(self, ccache=None, verbose=False, fallback=True,
delegate=False): delegate=False):
try: try:
xmlrpc_uri = self.env.xmlrpc_uri rpc_uri = self.env[self.env_rpc_uri_key]
principal = get_current_principal() principal = get_current_principal()
setattr(context, 'principal', principal) setattr(context, 'principal', principal)
# We have a session cookie, try using the session URI to see if it # We have a session cookie, try using the session URI to see if it
# is still valid # is still valid
if not delegate: if not delegate:
xmlrpc_uri = self.apply_session_cookie(xmlrpc_uri) rpc_uri = self.apply_session_cookie(rpc_uri)
except ValueError: except ValueError:
# No session key, do full Kerberos auth # No session key, do full Kerberos auth
pass pass
urls = self.get_url_list(xmlrpc_uri) urls = self.get_url_list(rpc_uri)
serverproxy = None serverproxy = None
for url in urls: for url in urls:
kw = dict(allow_none=True, encoding='UTF-8') kw = dict(allow_none=True, encoding='UTF-8')
kw['verbose'] = verbose kw['verbose'] = verbose
if url.startswith('https://'): if url.startswith('https://'):
if delegate: if delegate:
kw['transport'] = DelegatedKerbTransport() transport_class = DelegatedKerbTransport
else: else:
kw['transport'] = KerbTransport() transport_class = KerbTransport
else: else:
kw['transport'] = LanguageAwareTransport() transport_class = LanguageAwareTransport
kw['transport'] = transport_class(protocol=self.protocol)
self.log.debug('trying %s' % url) self.log.debug('trying %s' % url)
setattr(context, 'request_url', url) setattr(context, 'request_url', url)
serverproxy = ServerProxy(url, **kw) serverproxy = self.server_proxy_class(url, **kw)
if len(urls) == 1: if len(urls) == 1:
# if we have only 1 server and then let the # if we have only 1 server and then let the
# main requester handle any errors. This also means it # main requester handle any errors. This also means it
@@ -634,11 +757,11 @@ class xmlclient(Connectible):
try: try:
command = getattr(serverproxy, 'ping') command = getattr(serverproxy, 'ping')
try: try:
response = command() response = command([], {})
except Fault, e: except Fault, e:
e = decode_fault(e) e = decode_fault(e)
if e.faultCode in self.__errors: if e.faultCode in errors_by_code:
error = self.__errors[e.faultCode] error = errors_by_code[e.faultCode]
raise error(message=e.faultString) raise error(message=e.faultString)
else: else:
raise UnknownError( raise UnknownError(
@@ -683,6 +806,12 @@ class xmlclient(Connectible):
conn = conn.conn._ServerProxy__transport conn = conn.conn._ServerProxy__transport
conn.close() conn.close()
def _call_command(self, command, params):
"""Call the command with given params"""
# For XML, this method will wrap/unwrap binary values
# For JSON we do that in the proxy
return command(*params)
def forward(self, name, *args, **kw): def forward(self, name, *args, **kw):
""" """
Forward call to command named ``name`` over XML-RPC. Forward call to command named ``name`` over XML-RPC.
@@ -699,18 +828,18 @@ class xmlclient(Connectible):
'%s.forward(): %r not in api.Command' % (self.name, name) '%s.forward(): %r not in api.Command' % (self.name, name)
) )
server = getattr(context, 'request_url', None) server = getattr(context, 'request_url', None)
self.debug("Forwarding '%s' to server '%s'", name, server) self.debug("Forwarding '%s' to %s server '%s'",
name, self.protocol, server)
command = getattr(self.conn, name) command = getattr(self.conn, name)
params = [args, kw] params = [args, kw]
try: try:
response = command(*xml_wrap(params)) return self._call_command(command, params)
return xml_unwrap(response)
except Fault, e: except Fault, e:
e = decode_fault(e) e = decode_fault(e)
self.debug('Caught fault %d from server %s: %s', e.faultCode, self.debug('Caught fault %d from server %s: %s', e.faultCode,
server, e.faultString) server, e.faultString)
if e.faultCode in self.__errors: if e.faultCode in errors_by_code:
error = self.__errors[e.faultCode] error = errors_by_code[e.faultCode]
raise error(message=e.faultString) raise error(message=e.faultString)
raise UnknownError( raise UnknownError(
code=e.faultCode, code=e.faultCode,
@@ -756,3 +885,75 @@ class xmlclient(Connectible):
raise NetworkError(uri=server, error=str(e)) raise NetworkError(uri=server, error=str(e))
except (OverflowError, TypeError), e: except (OverflowError, TypeError), e:
raise XMLRPCMarshallError(error=str(e)) raise XMLRPCMarshallError(error=str(e))
class xmlclient(RPCClient):
session_path = '/ipa/session/xml'
server_proxy_class = ServerProxy
protocol = 'xml'
env_rpc_uri_key = 'xmlrpc_uri'
def _call_command(self, command, params):
params = xml_wrap(params)
result = command(*params)
return xml_unwrap(result)
class JSONServerProxy(object):
def __init__(self, uri, transport, encoding, verbose, allow_none):
type, uri = urllib.splittype(uri)
if type not in ("http", "https"):
raise IOError("unsupported XML-RPC protocol")
self.__host, self.__handler = urllib.splithost(uri)
self.__transport = transport
assert encoding == 'UTF-8'
assert allow_none
self.__verbose = verbose
# FIXME: Some of our code requires ServerProxy internals.
# But, xmlrpclib.ServerProxy's _ServerProxy__transport can be accessed
# by calling serverproxy('transport')
self._ServerProxy__transport = transport
def __request(self, name, args):
payload = {'method': unicode(name), 'params': args, 'id': 0}
response = self.__transport.request(
self.__host,
self.__handler,
json.dumps(json_encode_binary(payload)),
verbose=self.__verbose,
)
try:
response = json_decode_binary(json.loads(response))
except ValueError, e:
raise JSONError(str(e))
error = response.get('error')
if error:
try:
error_class = errors_by_code[error['code']]
except KeyError:
raise UnknownError(
code=error.get('code'),
error=error.get('message'),
server=self.__host,
)
else:
raise error_class(message=error['message'])
return response['result']
def __getattr__(self, name):
def _call(*args):
return self.__request(name, args)
return _call
class jsonclient(RPCClient):
session_path = '/ipa/session/json'
server_proxy_class = JSONServerProxy
protocol = 'json'
env_rpc_uri_key = 'jsonrpc_uri'

View File

@@ -162,9 +162,9 @@ class IpaAdvise(admintool.AdminTool):
advice.set_options(self.options) advice.set_options(self.options)
# Print out the actual advice # Print out the actual advice
api.Backend.xmlclient.connect() api.Backend.rpcclient.connect()
advice.get_info() advice.get_info()
api.Backend.xmlclient.disconnect() api.Backend.rpcclient.disconnect()
for line in advice.log.content: for line in advice.log.content:
print line print line

View File

@@ -25,27 +25,29 @@ Also see the `ipalib.rpc` module.
from xml.sax.saxutils import escape from xml.sax.saxutils import escape
from xmlrpclib import Fault from xmlrpclib import Fault
from wsgiref.util import shift_path_info
import base64
import os import os
import string
import datetime import datetime
from decimal import Decimal
import urlparse import urlparse
import time import time
import json import json
from ipalib import plugable, capabilities from ipalib import plugable, capabilities
from ipalib.backend import Executioner from ipalib.backend import Executioner
from ipalib.errors import PublicError, InternalError, CommandError, JSONError, ConversionError, CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError, ExecutionError from ipalib.errors import (PublicError, InternalError, CommandError, JSONError,
from ipalib.request import context, Connection, destroy_context CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError,
from ipalib.rpc import xml_dumps, xml_loads ExecutionError)
from ipalib.request import context, destroy_context
from ipalib.rpc import (xml_dumps, xml_loads,
json_encode_binary, json_decode_binary)
from ipalib.util import parse_time_duration, normalize_name from ipalib.util import parse_time_duration, normalize_name
from ipapython.dn import DN from ipapython.dn import DN
from ipaserver.plugins.ldap2 import ldap2 from ipaserver.plugins.ldap2 import ldap2
from ipalib.session import session_mgr, AuthManager, get_ipa_ccache_name, load_ccache_data, bind_ipa_ccache, release_ipa_ccache, fmt_time, default_max_session_duration from ipalib.session import (session_mgr, AuthManager, get_ipa_ccache_name,
load_ccache_data, bind_ipa_ccache, release_ipa_ccache, fmt_time,
default_max_session_duration)
from ipalib.backend import Backend from ipalib.backend import Backend
from ipalib.krb_utils import krb5_parse_ccache, KRB5_CCache, krb_ticket_expiration_threshold, krb5_format_principal_name from ipalib.krb_utils import (
KRB5_CCache, krb_ticket_expiration_threshold, krb5_format_principal_name)
from ipapython import ipautil from ipapython import ipautil
from ipapython.version import VERSION from ipapython.version import VERSION
from ipalib.text import _ from ipalib.text import _
@@ -397,99 +399,6 @@ class WSGIExecutioner(Executioner):
raise NotImplementedError('%s.marshal()' % self.fullname) raise NotImplementedError('%s.marshal()' % self.fullname)
def json_encode_binary(val):
'''
JSON cannot encode binary values. We encode binary values in Python str
objects and text in Python unicode objects. In order to allow a binary
object to be passed through JSON we base64 encode it thus converting it to
text which JSON can transport. To assure we recognize the value is a base64
encoded representation of the original binary value and not confuse it with
other text we convert the binary value to a dict in this form:
{'__base64__' : base64_encoding_of_binary_value}
This modification of the original input value cannot be done "in place" as
one might first assume (e.g. replacing any binary items in a container
(e.g. list, tuple, dict) with the base64 dict because the container might be
an immutable object (i.e. a tuple). Therefore this function returns a copy
of any container objects it encounters with tuples replaced by lists. This
is O.K. because the JSON encoding will map both lists and tuples to JSON
arrays.
'''
if isinstance(val, dict):
new_dict = {}
for k,v in val.items():
new_dict[k] = json_encode_binary(v)
return new_dict
elif isinstance(val, (list, tuple)):
new_list = [json_encode_binary(v) for v in val]
return new_list
elif isinstance(val, str):
return {'__base64__' : base64.b64encode(val)}
elif isinstance(val, Decimal):
return {'__base64__' : base64.b64encode(str(val))}
elif isinstance(val, DN):
return str(val)
else:
return val
def json_decode_binary(val):
'''
JSON cannot transport binary data. In order to transport binary data we
convert binary data to a form like this:
{'__base64__' : base64_encoding_of_binary_value}
see json_encode_binary()
After JSON had decoded the JSON stream back into a Python object we must
recursively scan the object looking for any dicts which might represent
binary values and replace the dict containing the base64 encoding of the
binary value with the decoded binary value. Unlike the encoding problem
where the input might consist of immutable object, all JSON decoded
container are mutable so the conversion could be done in place. However we
don't modify objects in place because of side effects which may be
dangerous. Thus we elect to spend a few more cycles and avoid the
possibility of unintended side effects in favor of robustness.
'''
if isinstance(val, dict):
if val.has_key('__base64__'):
return base64.b64decode(val['__base64__'])
else:
new_dict = {}
for k,v in val.items():
if isinstance(v, dict) and v.has_key('__base64__'):
new_dict[k] = base64.b64decode(v['__base64__'])
else:
new_dict[k] = json_decode_binary(v)
return new_dict
elif isinstance(val, list):
new_list = []
n = len(val)
i = 0
while i < n:
v = val[i]
if isinstance(v, dict) and v.has_key('__base64__'):
binary_val = base64.b64decode(v['__base64__'])
new_list.append(binary_val)
else:
new_list.append(json_decode_binary(v))
i += 1
return new_list
else:
if isinstance(val, basestring):
try:
return val.decode('utf-8')
except UnicodeDecodeError:
raise ConversionError(
name=val,
error='incorrect type'
)
else:
return val
class jsonserver(WSGIExecutioner, HTTP_Status): class jsonserver(WSGIExecutioner, HTTP_Status):
""" """
JSON RPC server. JSON RPC server.

View File

@@ -25,8 +25,8 @@ class TestCLIParsing(object):
def run_command(self, command_name, **kw): def run_command(self, command_name, **kw):
"""Run a command on the server""" """Run a command on the server"""
if not api.Backend.xmlclient.isconnected(): if not api.Backend.rpcclient.isconnected():
api.Backend.xmlclient.connect(fallback=False) api.Backend.rpcclient.connect(fallback=False)
try: try:
api.Command[command_name](**kw) api.Command[command_name](**kw)
except errors.NetworkError: except errors.NetworkError:

View File

@@ -241,7 +241,7 @@ class test_jsonserver(PluginTester):
assert unicode(e.error) == 'params[1] (aka options) must be a dict' assert unicode(e.error) == 'params[1] (aka options) must be a dict'
# Test with valid values: # Test with valid values:
args = [u'jdoe'] args = (u'jdoe', )
options = dict(givenname=u'John', sn='Doe') options = dict(givenname=u'John', sn='Doe')
d = dict(method=u'user_add', params=[args, options], id=18) d = dict(method=u'user_add', params=(args, options), id=18)
assert o.unmarshal(json.dumps(d)) == (u'user_add', args, options, 18) assert o.unmarshal(json.dumps(d)) == (u'user_add', args, options, 18)

View File

@@ -63,8 +63,8 @@ class test_dns(Declarative):
def setUpClass(cls): def setUpClass(cls):
super(test_dns, cls).setUpClass() super(test_dns, cls).setUpClass()
if not api.Backend.xmlclient.isconnected(): if not api.Backend.rpcclient.isconnected():
api.Backend.xmlclient.connect(fallback=False) api.Backend.rpcclient.connect(fallback=False)
try: try:
api.Command['dnszone_add'](dnszone1, api.Command['dnszone_add'](dnszone1,
idnssoamname = dnszone1_mname, idnssoamname = dnszone1_mname,

View File

@@ -45,8 +45,8 @@ class test_external_members(Declarative):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(test_external_members, cls).setUpClass() super(test_external_members, cls).setUpClass()
if not api.Backend.xmlclient.isconnected(): if not api.Backend.rpcclient.isconnected():
api.Backend.xmlclient.connect(fallback=False) api.Backend.rpcclient.connect(fallback=False)
trusts = api.Command['trust_find']() trusts = api.Command['trust_find']()
if trusts['count'] == 0: if trusts['count'] == 0:

View File

@@ -41,8 +41,8 @@ class test_trustconfig(Declarative):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(test_trustconfig, cls).setUpClass() super(test_trustconfig, cls).setUpClass()
if not api.Backend.xmlclient.isconnected(): if not api.Backend.rpcclient.isconnected():
api.Backend.xmlclient.connect(fallback=False) api.Backend.rpcclient.connect(fallback=False)
try: try:
api.Command['trustconfig_show'](trust_type=u'ad') api.Command['trustconfig_show'](trust_type=u'ad')
except errors.NotFound: except errors.NotFound:

View File

@@ -86,8 +86,8 @@ def fuzzy_set_ci(s):
return Fuzzy(test=lambda other: set(x.lower() for x in other) == set(y.lower() for y in s)) return Fuzzy(test=lambda other: set(x.lower() for x in other) == set(y.lower() for y in s))
try: try:
if not api.Backend.xmlclient.isconnected(): if not api.Backend.rpcclient.isconnected():
api.Backend.xmlclient.connect(fallback=False) api.Backend.rpcclient.connect(fallback=False)
res = api.Command['user_show'](u'notfound') res = api.Command['user_show'](u'notfound')
except errors.NetworkError: except errors.NetworkError:
server_available = False server_available = False
@@ -163,8 +163,8 @@ class XMLRPC_test(object):
(cls.__module__, api.env.xmlrpc_uri)) (cls.__module__, api.env.xmlrpc_uri))
def setUp(self): def setUp(self):
if not api.Backend.xmlclient.isconnected(): if not api.Backend.rpcclient.isconnected():
api.Backend.xmlclient.connect(fallback=False) api.Backend.rpcclient.connect(fallback=False)
def tearDown(self): def tearDown(self):
""" """