svn keywords
[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
18 # See "2.2 Characters" in the XML specification:
19 #
20 # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
21 # avoiding
22 # [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
23
24 invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F))
25 xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii))
26
27 def xmlrpclib_escape(s, replace = string.replace):
28     """
29     xmlrpclib does not handle invalid 7-bit control characters. This
30     function augments xmlrpclib.escape, which by default only replaces
31     '&', '<', and '>' with entities.
32     """
33
34     # This is the standard xmlrpclib.escape function
35     s = replace(s, "&", "&amp;")
36     s = replace(s, "<", "&lt;")
37     s = replace(s, ">", "&gt;",)
38
39     # Replace invalid 7-bit control characters with '?'
40     return s.translate(xml_escape_table)
41
42 def xmlrpclib_dump(self, value, write):
43     """
44     xmlrpclib cannot marshal instances of subclasses of built-in
45     types. This function overrides xmlrpclib.Marshaller.__dump so that
46     any value that is an instance of one of its acceptable types is
47     marshalled as that type.
48
49     xmlrpclib also cannot handle invalid 7-bit control characters. See
50     above.
51     """
52
53     # Use our escape function
54     args = [self, value, write]
55     if isinstance(value, (str, unicode)):
56         args.append(xmlrpclib_escape)
57
58     try:
59         # Try for an exact match first
60         f = self.dispatch[type(value)]
61     except KeyError:
62         # Try for an isinstance() match
63         for Type, f in self.dispatch.iteritems():
64             if isinstance(value, Type):
65                 f(*args)
66                 return
67         raise TypeError, "cannot marshal %s objects" % type(value)
68     else:
69         f(*args)
70
71 # You can't hide from me!
72 xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump
73
74 # SOAP support is optional
75 try:
76     import SOAPpy
77     from SOAPpy.Parser import parseSOAPRPC
78     from SOAPpy.Types import faultType
79     from SOAPpy.NS import NS
80     from SOAPpy.SOAPBuilder import buildSOAP
81 except ImportError:
82     SOAPpy = None
83
84 from PLC.Config import Config
85 from PLC.Faults import *
86 import PLC.Methods
87 import PLC.Legacy
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     legacy_methods = PLC.Legacy.native_methods
102
103     # other_methods_map : dict {methodname: fullpath}
104     # e.g. 'Accessors' -> 'PLC.Accessors.Accessors'
105     other_methods_map={}
106     for subdir in [ 'Accessors' ]:
107         path="PLC."+subdir
108         # scan e.g. PLC.Accessors.__all__
109         pkg = __import__(path).__dict__[subdir]
110         for modulename in getattr(pkg,"__all__"):
111             fullpath=path+"."+modulename
112             for method in getattr(import_deep(fullpath),"methods"):
113                 other_methods_map[method] = fullpath
114
115     all_methods = native_methods + legacy_methods + other_methods_map.keys()
116     
117     def __init__(self, config = "/etc/planetlab/plc_config", encoding = "utf-8"):
118         self.encoding = encoding
119
120         # Better just be documenting the API
121         if config is None:
122             return
123
124         # Load configuration
125         self.config = Config(config)
126         
127         # Initialize database connection
128         if self.config.PLC_DB_TYPE == "postgresql":
129             from PLC.PostgreSQL import PostgreSQL
130             self.db = PostgreSQL(self)
131
132         else:
133             raise PLCAPIError, "Unsupported database type " + self.config.PLC_DB_TYPE
134
135     def callable(self, method):
136         """
137         Return a new instance of the specified method.
138         """
139
140         # Look up method
141         if method not in self.all_methods:
142             raise PLCInvalidAPIMethod, method
143         
144         # Get new instance of method
145         try:
146             classname = method.split(".")[-1]
147             if method in self.native_methods:
148                 fullpath="PLC.Methods." + method
149             elif method in self.legacy_methods:
150                 fullpath="PLC.Legacy." + method
151             else:
152                 fullpath=self.other_methods_map[method]
153             module = __import__(fullpath, globals(), locals(), [classname])
154             return getattr(module, classname)(self)
155         except ImportError, AttributeError:
156             raise PLCInvalidAPIMethod, "import error %s for %s" % (AttributeError,fullpath)
157
158     def call(self, source, method, *args):
159         """
160         Call the named method from the specified source with the
161         specified arguments.
162         """
163
164         function = self.callable(method)
165         function.source = source
166         return function(*args)
167
168     def handle(self, source, data):
169         """
170         Handle an XML-RPC or SOAP request from the specified source.
171         """
172
173         # Parse request into method name and arguments
174         try:
175             interface = xmlrpclib
176             (args, method) = xmlrpclib.loads(data)
177             methodresponse = True
178         except Exception, e:
179             if SOAPpy is not None:
180                 interface = SOAPpy
181                 (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
182                 method = r._name
183                 args = r._aslist()
184                 # XXX Support named arguments
185             else:
186                 raise e
187
188         try:
189             result = self.call(source, method, *args)
190         except PLCFault, fault:
191             # Handle expected faults
192             if interface == xmlrpclib:
193                 result = fault
194                 methodresponse = None
195             elif interface == SOAPpy:
196                 result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
197                 result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString))
198
199         # Return result
200         if interface == xmlrpclib:
201             if not isinstance(result, PLCFault):
202                 result = (result,)
203             data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1)
204         elif interface == SOAPpy:
205             data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding)
206
207         return data