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