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