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