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