Remove debug line.
[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         logger.log_exc()
63
64 @export_to_api(0)
65 def GetXIDs():
66     """GetXIDs(): return an dictionary mapping slice names to XIDs"""
67     return dict([(pwent[0], pwent[2]) for pwent in pwd.getpwall() if pwent[6] == sliver_vs.Sliver_VS.SHELL])
68
69 @export_to_api(0)
70 def GetSSHKeys():
71     """GetSSHKeys(): return an dictionary mapping slice names to SSH keys"""
72     keydict = {}
73     for rec in database.db.itervalues():
74         if 'keys' in rec:
75             keydict[rec['name']] = rec['keys']
76     return keydict
77
78 @export_to_api(1)
79 def Create(rec):
80     """Create(sliver_name): create a non-PLC-instantiated sliver"""
81     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_created(rec)
82
83 @export_to_api(1)
84 def Destroy(rec):
85     """Destroy(sliver_name): destroy a non-PLC-instantiated sliver"""
86     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_destroyed()
87
88 @export_to_api(1)
89 def Start(rec):
90     """Start(sliver_name): run start scripts belonging to the specified sliver"""
91     accounts.get(rec['name']).start()
92
93 @export_to_api(1)
94 def Stop(rec):
95     """Stop(sliver_name): kill all processes belonging to the specified sliver"""
96     accounts.get(rec['name']).stop()
97
98 @export_to_api(1)
99 def GetEffectiveRSpec(rec):
100     """GetEffectiveRSpec(sliver_name): return the RSpec allocated to the specified sliver, including loans"""
101     return rec.get('_rspec', {}).copy()
102
103 @export_to_api(1)
104 def GetRSpec(rec):
105     """GetRSpec(sliver_name): return the RSpec allocated to the specified sliver, excluding loans"""
106     return rec.get('rspec', {}).copy()
107
108 @export_to_api(1)
109 def GetLoans(rec):
110     """GetLoans(sliver_name): return the list of loans made by the specified sliver"""
111     return rec.get('_loans', [])[:]
112
113 def validate_loans(obj):
114     """Check that <obj> is a valid loan specification."""
115     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
116     return type(obj)==list and False not in map(validate_loan, obj)
117
118 @export_to_api(2)
119 def SetLoans(rec, loans):
120     """SetLoans(sliver_name, loans): overwrite the list of loans made by the specified sliver"""
121     if not validate_loans(loans): raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
122     rec['_loans'] = loans
123     database.db.sync()
124
125
126 class APIRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
127     # overriding _dispatch to achieve this effect is officially deprecated,
128     # but I can't figure out how to get access to .request without
129     # duplicating SimpleXMLRPCServer code here, which is more likely to
130     # change than the deprecated behavior is to be broken
131
132     @database.synchronized
133     def _dispatch(self, method_name_unicode, args):
134         method_name = str(method_name_unicode)
135         try: method = api_method_dict[method_name]
136         except KeyError:
137             api_method_list = api_method_dict.keys()
138             api_method_list.sort()
139             raise xmlrpclib.Fault(100, 'Invalid API method %s.  Valid choices are %s' % (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.' % (len(args), 
143             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, 'Invalid argument: the first argument must be a sliver name.')
157                 if not (caller_name, method_name) in target_rec['delegations']:
158                # or (caller_name == 'utah_elab_delegate' and target_name.startswith('utah_'))): 
159                     raise xmlrpclib.Fault(108, 'Permission denied.')
160
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)