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