- raise an xmlrpclib.Fault on any exception while parsing/delivering ticket
[nodemanager.git] / api.py
1 """Sliver manager API.
2
3 This module exposes an XMLRPC interface that allows PlanetLab users to
4 create/destroy slivers with delegated instantiation, start and stop
5 slivers, make resource loans, and examine resource allocations.  The
6 XMLRPC is provided on a localhost-only TCP port as well as via a Unix
7 domain socket that is accessible by ssh-ing into a delegate account
8 with the forward_api_calls shell.
9 """
10
11 import SimpleXMLRPCServer
12 import SocketServer
13 import errno
14 import os
15 import pwd
16 import socket
17 import struct
18 import threading
19 import xmlrpclib
20
21 import accounts
22 import database
23 import logger
24 import ticket
25 import tools
26
27
28 API_SERVER_PORT = 812
29 UNIX_ADDR = '/tmp/sliver_mgr.api'
30
31 deliver_ticket = None  # set in sm.py:start()
32
33 api_method_dict = {}
34 nargs_dict = {}
35
36 def export_to_api(nargs):
37     def export(method):
38         nargs_dict[method.__name__] = nargs
39         api_method_dict[method.__name__] = method
40         return method
41     return export
42
43
44 @export_to_api(0)
45 def Help():
46     """Help(): get help"""
47     return ''.join([method.__doc__ + '\n' for method in api_method_dict.itervalues()])
48
49 @export_to_api(1)
50 def Ticket(tkt):
51     """Ticket(tkt): deliver a ticket"""
52     try:
53         data = ticket.verify(tkt)
54         if data != None:
55             deliver_ticket(data)
56     except Exception, err:
57         raise xmlrpclib.Fault(102, 'Ticket error: ' + str(err))
58
59 @export_to_api(1)
60 def Create(rec):
61     """Create(sliver_name): create a non-PLC-instantiated sliver"""
62     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_created(rec)
63
64 @export_to_api(1)
65 def Destroy(rec):
66     """Destroy(sliver_name): destroy a non-PLC-instantiated sliver"""
67     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_destroyed()
68
69 @export_to_api(1)
70 def Start(rec):
71     """Start(sliver_name): run start scripts belonging to the specified sliver"""
72     accounts.get(rec['name']).start()
73
74 @export_to_api(1)
75 def Stop(rec):
76     """Stop(sliver_name): kill all processes belonging to the specified sliver"""
77     accounts.get(rec['name']).stop()
78
79 @export_to_api(1)
80 def GetEffectiveRSpec(rec):
81     """GetEffectiveRSpec(sliver_name): return the RSpec allocated to the specified sliver, including loans"""
82     return rec.get('_rspec', {}).copy()
83
84 @export_to_api(1)
85 def GetRSpec(rec):
86     """GetRSpec(sliver_name): return the RSpec allocated to the specified sliver, excluding loans"""
87     return rec.get('rspec', {}).copy()
88
89 @export_to_api(1)
90 def GetLoans(rec):
91     """GetLoans(sliver_name): return the list of loans made by the specified sliver"""
92     return rec.get('_loans', [])[:]
93
94 def validate_loans(obj):
95     """Check that <obj> is a valid loan specification."""
96     def validate_loan(obj): return (type(obj)==list or type(obj)==tuple) and len(obj)==3 and type(obj[0])==str and type(obj[1])==str and obj[1] in database.LOANABLE_RESOURCES and type(obj[2])==int and obj[2]>=0
97     return type(obj)==list and False not in map(validate_loan, obj)
98
99 @export_to_api(2)
100 def SetLoans(rec, loans):
101     """SetLoans(sliver_name, loans): overwrite the list of loans made by the specified sliver"""
102     if not validate_loans(loans): raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
103     rec['_loans'] = loans
104     database.db.sync()
105
106
107 class APIRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
108     # overriding _dispatch to achieve this effect is officially deprecated,
109     # but I can't figure out how to get access to .request without
110     # duplicating SimpleXMLRPCServer code here, which is more likely to
111     # change than the deprecated behavior is to be broken
112
113     @database.synchronized
114     def _dispatch(self, method_name_unicode, args):
115         method_name = str(method_name_unicode)
116         try: method = api_method_dict[method_name]
117         except KeyError:
118             api_method_list = api_method_dict.keys()
119             api_method_list.sort()
120             raise xmlrpclib.Fault(100, 'Invalid API method %s.  Valid choices are %s' % (method_name, ', '.join(api_method_list)))
121         expected_nargs = nargs_dict[method_name]
122         if len(args) != expected_nargs: raise xmlrpclib.Fault(101, 'Invalid argument count: got %d, expecting %d.' % (len(args), expected_nargs))
123         else:
124             # Figure out who's calling.
125             # XXX - these ought to be imported directly from some .h file
126             SO_PEERCRED = 17
127             sizeof_struct_ucred = 12
128             ucred = self.request.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, sizeof_struct_ucred)
129             xid = struct.unpack('3i', ucred)[2]
130             caller_name = pwd.getpwuid(xid)[0]
131             if method_name not in ('Help', 'Ticket'):
132                 target_name = args[0]
133                 target_rec = database.db.get(target_name)
134                 if not (target_rec and target_rec['type'].startswith('sliver.')): raise xmlrpclib.Fault(102, 'Invalid argument: the first argument must be a sliver name.')
135                 if not (caller_name in (args[0], 'root') or (caller_name, method_name) in target_rec['delegations']): raise xmlrpclib.Fault(108, 'Permission denied.')
136                 result = method(target_rec, *args[1:])
137             else: result = method(*args)
138             if result == None: result = 1
139             return result
140
141 class APIServer_INET(SocketServer.ThreadingMixIn, SimpleXMLRPCServer.SimpleXMLRPCServer): allow_reuse_address = True
142
143 class APIServer_UNIX(APIServer_INET): address_family = socket.AF_UNIX
144
145 def start():
146     """Start two XMLRPC interfaces: one bound to localhost, the other bound to a Unix domain socket."""
147     serv1 = APIServer_INET(('127.0.0.1', API_SERVER_PORT), requestHandler=APIRequestHandler, logRequests=0)
148     tools.as_daemon_thread(serv1.serve_forever)
149     try: os.unlink(UNIX_ADDR)
150     except OSError, e:
151         if e.errno != errno.ENOENT: raise
152     serv2 = APIServer_UNIX(UNIX_ADDR, requestHandler=APIRequestHandler, logRequests=0)
153     tools.as_daemon_thread(serv2.serve_forever)
154     os.chmod(UNIX_ADDR, 0666)