8d95f8e36d847eeebbedc4a3c966ed4a14419603
[plcapi.git] / PLC / API.py
1 #
2 # PLCAPI XML-RPC and SOAP interfaces
3 #
4 # Aaron Klingaman <alk@absarokasoft.com>
5 # Mark Huang <mlhuang@cs.princeton.edu>
6 #
7 # Copyright (C) 2004-2006 The Trustees of Princeton University
8 # $Id$
9 # $URL$
10 #
11
12 import sys
13 import traceback
14 import string
15
16 import xmlrpclib
17 import simplejson
18
19 # See "2.2 Characters" in the XML specification:
20 #
21 # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
22 # avoiding
23 # [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
24
25 invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F))
26 xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii))
27
28 def xmlrpclib_escape(s, replace = string.replace):
29     """
30     xmlrpclib does not handle invalid 7-bit control characters. This
31     function augments xmlrpclib.escape, which by default only replaces
32     '&', '<', and '>' with entities.
33     """
34
35     # This is the standard xmlrpclib.escape function
36     s = replace(s, "&", "&amp;")
37     s = replace(s, "<", "&lt;")
38     s = replace(s, ">", "&gt;",)
39
40     # Replace invalid 7-bit control characters with '?'
41     return s.translate(xml_escape_table)
42
43 def xmlrpclib_dump(self, value, write):
44     """
45     xmlrpclib cannot marshal instances of subclasses of built-in
46     types. This function overrides xmlrpclib.Marshaller.__dump so that
47     any value that is an instance of one of its acceptable types is
48     marshalled as that type.
49
50     xmlrpclib also cannot handle invalid 7-bit control characters. See
51     above.
52     """
53
54     # Use our escape function
55     args = [self, value, write]
56     if isinstance(value, (str, unicode)):
57         args.append(xmlrpclib_escape)
58
59     try:
60         # Try for an exact match first
61         f = self.dispatch[type(value)]
62     except KeyError:
63         # Try for an isinstance() match
64         for Type, f in self.dispatch.iteritems():
65             if isinstance(value, Type):
66                 f(*args)
67                 return
68         raise TypeError, "cannot marshal %s objects" % type(value)
69     else:
70         f(*args)
71
72 # You can't hide from me!
73 xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump
74
75 # SOAP support is optional
76 try:
77     import SOAPpy
78     from SOAPpy.Parser import parseSOAPRPC
79     from SOAPpy.Types import faultType
80     from SOAPpy.NS import NS
81     from SOAPpy.SOAPBuilder import buildSOAP
82 except ImportError:
83     SOAPpy = None
84
85 from PLC.Config import Config
86 from PLC.Faults import *
87 import PLC.Methods
88 import PLC.Accessors
89
90 def import_deep(name):
91     mod = __import__(name)
92     components = name.split('.')
93     for comp in components[1:]:
94         mod = getattr(mod, comp)
95     return mod
96
97 class PLCAPI:
98
99     # flat list of method names
100     native_methods = PLC.Methods.native_methods
101
102     # other_methods_map : dict {methodname: fullpath}
103     # e.g. 'Accessors' -> 'PLC.Accessors.Accessors'
104     other_methods_map={}
105     for subdir in [ 'Accessors' ]:
106         path="PLC."+subdir
107         # scan e.g. PLC.Accessors.__all__
108         pkg = __import__(path).__dict__[subdir]
109         for modulename in getattr(pkg,"__all__"):
110             fullpath=path+"."+modulename
111             for method in getattr(import_deep(fullpath),"methods"):
112                 other_methods_map[method] = fullpath
113
114     all_methods = native_methods + other_methods_map.keys()
115
116     def __init__(self, config = "/etc/planetlab/plc_config", encoding = "utf-8"):
117         self.encoding = encoding
118
119         # Better just be documenting the API
120         if config is None:
121             return
122
123         # Load configuration
124         self.config = Config(config)
125
126         # Initialize database connection
127         if self.config.PLC_DB_TYPE == "postgresql":
128             from PLC.PostgreSQL import PostgreSQL
129             self.db = PostgreSQL(self)
130         else:
131             raise PLCAPIError, "Unsupported database type " + self.config.PLC_DB_TYPE
132
133         # Aspects modify the API injecting code before/after method
134         # calls. As of now we only have aspects for OMF integration,
135         # that's why we enable aspects only if PLC_OMF is set to true.
136         if self.config.PLC_OMF_ENABLED:
137             from aspects import apply_aspects; apply_aspects()
138
139
140     def callable(self, method):
141         """
142         Return a new instance of the specified method.
143         """
144
145         # Look up method
146         if method not in self.all_methods:
147             raise PLCInvalidAPIMethod, method
148
149         # Get new instance of method
150         try:
151             classname = method.split(".")[-1]
152             if method in self.native_methods:
153                 fullpath="PLC.Methods." + method
154             else:
155                 fullpath=self.other_methods_map[method]
156             module = __import__(fullpath, globals(), locals(), [classname])
157             return getattr(module, classname)(self)
158         except ImportError, AttributeError:
159             raise PLCInvalidAPIMethod, "import error %s for %s" % (AttributeError,fullpath)
160
161     def call(self, source, method, *args):
162         """
163         Call the named method from the specified source with the
164         specified arguments.
165         """
166
167         function = self.callable(method)
168         function.source = source
169         return function(*args)
170
171     def handle(self, source, data):
172         """
173         Handle an XML-RPC or SOAP request from the specified source.
174         """
175
176         # Parse request into method name and arguments
177         try:
178             interface = xmlrpclib
179             (args, method) = xmlrpclib.loads(data)
180             methodresponse = True
181         except Exception, e:
182             if SOAPpy is not None:
183                 interface = SOAPpy
184                 (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
185                 method = r._name
186                 args = r._aslist()
187                 # XXX Support named arguments
188             else:
189                 raise e
190
191         try:
192             result = self.call(source, method, *args)
193         except PLCFault, fault:
194             # Handle expected faults
195             if interface == xmlrpclib:
196                 result = fault
197                 methodresponse = None
198             elif interface == SOAPpy:
199                 result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
200                 result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString))
201
202         # Return result
203         if interface == xmlrpclib:
204             if not isinstance(result, PLCFault):
205                 result = (result,)
206             data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1)
207         elif interface == SOAPpy:
208             data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding)
209
210         return data
211
212     def handle_json(self, source, data):
213         """
214         Handle a JSON request 
215         """
216         method, args = simplejson.loads(data)
217         try:
218             result = self.call(source, method, *args)
219         except Exception, e:
220             result = str(e)
221        
222         return simplejson.dumps(result) 
223         
224