Removed bad method check in api dispatch.
[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 sliver_vs
25 import ticket
26 import tools
27
28
29 API_SERVER_PORT = 812
30 UNIX_ADDR = '/tmp/sliver_mgr.api'
31
32 deliver_ticket = None  # set in sm.py:start()
33
34 api_method_dict = {}
35 nargs_dict = {}
36
37 def export_to_api(nargs):
38     def export(method):
39         nargs_dict[method.__name__] = nargs
40         api_method_dict[method.__name__] = method
41         return method
42     return export
43
44
45 @export_to_api(0)
46 def Help():
47     """Help(): get help"""
48     return ''.join([method.__doc__ + '\n' for method in api_method_dict.itervalues()])
49
50 @export_to_api(1)
51 def Ticket(tkt):
52     """Ticket(tkt): deliver a ticket"""
53     try:
54         data = ticket.verify(tkt)
55         name = data['slivers'][0]['name']
56         if data != None:
57             deliver_ticket(data)
58         logger.log('Ticket delivered for %s' % name)
59         Create(database.db.get(name))
60     except Exception, err:
61         raise xmlrpclib.Fault(102, 'Ticket error: ' + str(err))
62
63 @export_to_api(0)
64 def GetXIDs():
65     """GetXIDs(): return an dictionary mapping slice names to XIDs"""
66     return dict([(pwent[0], pwent[2]) for pwent in pwd.getpwall() if pwent[6] == sliver_vs.Sliver_VS.SHELL])
67
68 @export_to_api(0)
69 def GetSSHKeys():
70     """GetSSHKeys(): return an dictionary mapping slice names to SSH keys"""
71     keydict = {}
72     for rec in database.db.itervalues():
73         if 'keys' in rec:
74             keydict[rec['name']] = rec['keys']
75     return keydict
76
77 @export_to_api(1)
78 def Create(rec):
79     """Create(sliver_name): create a non-PLC-instantiated sliver"""
80     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_created(rec)
81
82 @export_to_api(1)
83 def Destroy(rec):
84     """Destroy(sliver_name): destroy a non-PLC-instantiated sliver"""
85     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_destroyed()
86
87 @export_to_api(1)
88 def Start(rec):
89     """Start(sliver_name): run start scripts belonging to the specified sliver"""
90     accounts.get(rec['name']).start()
91
92 @export_to_api(1)
93 def Stop(rec):
94     """Stop(sliver_name): kill all processes belonging to the specified sliver"""
95     accounts.get(rec['name']).stop()
96
97 @export_to_api(1)
98 def GetEffectiveRSpec(rec):
99     """GetEffectiveRSpec(sliver_name): return the RSpec allocated to the specified sliver, including loans"""
100     return rec.get('_rspec', {}).copy()
101
102 @export_to_api(1)
103 def GetRSpec(rec):
104     """GetRSpec(sliver_name): return the RSpec allocated to the specified sliver, excluding loans"""
105     return rec.get('rspec', {}).copy()
106
107 @export_to_api(1)
108 def GetLoans(rec):
109     """GetLoans(sliver_name): return the list of loans made by the specified sliver"""
110     return rec.get('_loans', [])[:]
111
112 def validate_loans(obj):
113     """Check that <obj> is a valid loan specification."""
114     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
115     return type(obj)==list and False not in map(validate_loan, obj)
116
117 @export_to_api(2)
118 def SetLoans(rec, loans):
119     """SetLoans(sliver_name, loans): overwrite the list of loans made by the specified sliver"""
120     if not validate_loans(loans): raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
121     rec['_loans'] = loans
122     database.db.sync()
123
124
125 class APIRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
126     # overriding _dispatch to achieve this effect is officially deprecated,
127     # but I can't figure out how to get access to .request without
128     # duplicating SimpleXMLRPCServer code here, which is more likely to
129     # change than the deprecated behavior is to be broken
130
131     @database.synchronized
132     def _dispatch(self, method_name_unicode, args):
133         method_name = str(method_name_unicode)
134         try: method = api_method_dict[method_name]
135         except KeyError:
136             api_method_list = api_method_dict.keys()
137             api_method_list.sort()
138             raise xmlrpclib.Fault(100, 'Invalid API method %s.  Valid choices are %s' % \
139                 (method_name, ', '.join(api_method_list)))
140         expected_nargs = nargs_dict[method_name]
141         if len(args) != expected_nargs: 
142             raise xmlrpclib.Fault(101, 'Invalid argument count: got %d, expecting %d.' % \
143                 (len(args), expected_nargs))
144         else:
145             # Figure out who's calling.
146             # XXX - these ought to be imported directly from some .h file
147             SO_PEERCRED = 17
148             sizeof_struct_ucred = 12
149             ucred = self.request.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, sizeof_struct_ucred)
150             xid = struct.unpack('3i', ucred)[2]
151             caller_name = pwd.getpwuid(xid)[0]
152             if method_name not in ('ReCreate', 'Help', 'Ticket', 'GetXIDs', 'GetSSHKeys'):
153                 target_name = args[0]
154                 target_rec = database.db.get(target_name)
155                 if not (target_rec and target_rec['type'].startswith('sliver.')): 
156                     raise xmlrpclib.Fault(102, 
157                         'Invalid argument: the first argument must be a sliver name.')
158                 if not caller_name in target_rec['delegations']:
159                # or (caller_name == 'utah_elab_delegate' and target_name.startswith('utah_'))): 
160                     raise xmlrpclib.Fault(108, 'Permission denied.')
161                 result = method(target_rec, *args[1:])
162             else: result = method(*args)
163             if result == None: result = 1
164             return result
165
166 class APIServer_INET(SocketServer.ThreadingMixIn, SimpleXMLRPCServer.SimpleXMLRPCServer): allow_reuse_address = True
167
168 class APIServer_UNIX(APIServer_INET): address_family = socket.AF_UNIX
169
170 def start():
171     """Start two XMLRPC interfaces: one bound to localhost, the other bound to a Unix domain socket."""
172     serv1 = APIServer_INET(('127.0.0.1', API_SERVER_PORT), requestHandler=APIRequestHandler, logRequests=0)
173     tools.as_daemon_thread(serv1.serve_forever)
174     try: os.unlink(UNIX_ADDR)
175     except OSError, e:
176         if e.errno != errno.ENOENT: raise
177     serv2 = APIServer_UNIX(UNIX_ADDR, requestHandler=APIRequestHandler, logRequests=0)
178     tools.as_daemon_thread(serv2.serve_forever)
179     os.chmod(UNIX_ADDR, 0666)