Added xml rpc ReCreate Method.
[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 ReCreate(rec):
86     """ReCreate(sliver_name): destroy then recreate 
87     and start sliver regardless of instantiation."""
88     accounts.get(rec['name']).ensure_destroyed()
89     accounts.get(rec['name']).ensure_created(rec)
90     accounts.get(rec['name']).start()
91
92 @export_to_api(1)
93 def Start(rec):
94     """Start(sliver_name): run start scripts belonging to the specified sliver"""
95     accounts.get(rec['name']).start()
96
97 @export_to_api(1)
98 def Stop(rec):
99     """Stop(sliver_name): kill all processes belonging to the specified sliver"""
100     accounts.get(rec['name']).stop()
101
102 @export_to_api(1)
103 def GetEffectiveRSpec(rec):
104     """GetEffectiveRSpec(sliver_name): return the RSpec allocated to the specified sliver, including loans"""
105     return rec.get('_rspec', {}).copy()
106
107 @export_to_api(1)
108 def GetRSpec(rec):
109     """GetRSpec(sliver_name): return the RSpec allocated to the specified sliver, excluding loans"""
110     return rec.get('rspec', {}).copy()
111
112 @export_to_api(1)
113 def GetLoans(rec):
114     """GetLoans(sliver_name): return the list of loans made by the specified sliver"""
115     return rec.get('_loans', [])[:]
116
117 def validate_loans(obj):
118     """Check that <obj> is a valid loan specification."""
119     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
120     return type(obj)==list and False not in map(validate_loan, obj)
121
122 @export_to_api(2)
123 def SetLoans(rec, loans):
124     """SetLoans(sliver_name, loans): overwrite the list of loans made by the specified sliver"""
125     if not validate_loans(loans): raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
126     rec['_loans'] = loans
127     database.db.sync()
128
129
130 class APIRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
131     # overriding _dispatch to achieve this effect is officially deprecated,
132     # but I can't figure out how to get access to .request without
133     # duplicating SimpleXMLRPCServer code here, which is more likely to
134     # change than the deprecated behavior is to be broken
135
136     @database.synchronized
137     def _dispatch(self, method_name_unicode, args):
138         method_name = str(method_name_unicode)
139         try: method = api_method_dict[method_name]
140         except KeyError:
141             api_method_list = api_method_dict.keys()
142             api_method_list.sort()
143             raise xmlrpclib.Fault(100, 'Invalid API method %s.  Valid choices are %s' % (method_name, ', '.join(api_method_list)))
144         expected_nargs = nargs_dict[method_name]
145         if len(args) != expected_nargs: raise xmlrpclib.Fault(101, 'Invalid argument count: got %d, expecting %d.' % (len(args), expected_nargs))
146         else:
147             # Figure out who's calling.
148             # XXX - these ought to be imported directly from some .h file
149             SO_PEERCRED = 17
150             sizeof_struct_ucred = 12
151             ucred = self.request.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, sizeof_struct_ucred)
152             xid = struct.unpack('3i', ucred)[2]
153             caller_name = pwd.getpwuid(xid)[0]
154             if method_name not in ('Help', 'Ticket', 'GetXIDs', 'GetSSHKeys'):
155                 target_name = args[0]
156                 target_rec = database.db.get(target_name)
157                 if not (target_rec and target_rec['type'].startswith('sliver.')): raise xmlrpclib.Fault(102, 'Invalid argument: the first argument must be a sliver name.')
158                 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_'))): raise xmlrpclib.Fault(108, 'Permission denied.')
159                 result = method(target_rec, *args[1:])
160             else: result = method(*args)
161             if result == None: result = 1
162             return result
163
164 class APIServer_INET(SocketServer.ThreadingMixIn, SimpleXMLRPCServer.SimpleXMLRPCServer): allow_reuse_address = True
165
166 class APIServer_UNIX(APIServer_INET): address_family = socket.AF_UNIX
167
168 def start():
169     """Start two XMLRPC interfaces: one bound to localhost, the other bound to a Unix domain socket."""
170     serv1 = APIServer_INET(('127.0.0.1', API_SERVER_PORT), requestHandler=APIRequestHandler, logRequests=0)
171     tools.as_daemon_thread(serv1.serve_forever)
172     try: os.unlink(UNIX_ADDR)
173     except OSError, e:
174         if e.errno != errno.ENOENT: raise
175     serv2 = APIServer_UNIX(UNIX_ADDR, requestHandler=APIRequestHandler, logRequests=0)
176     tools.as_daemon_thread(serv2.serve_forever)
177     os.chmod(UNIX_ADDR, 0666)