A few tweaks.
[nodemanager.git] / api_calls.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 try:
22         from PLC.Parameter import Parameter, Mixed
23 except:
24         pass
25
26 import accounts
27 import logger
28
29 # TODO: These try/excepts are a hack to allow doc/DocBookLocal.py to 
30 # import this file in order to extrac the documentation from each 
31 # exported function.  A better approach will involve more extensive code
32 # splitting, I think.
33 try: import database
34 except: import logger as database
35 try: import sliver_vs
36 except: import logger as sliver_vs
37 import ticket as ticket_module
38 import tools
39
40
41 api_method_dict = {}
42 nargs_dict = {}
43
44 def export_to_api(nargs):
45     def export(method):
46         nargs_dict[method.__name__] = nargs
47         api_method_dict[method.__name__] = method
48         return method
49     return export
50
51 def export_to_docbook(**kwargs):
52
53     keywords = {
54         "group" : "NMAPI",
55         "status" : "current",
56         "name": None,
57         "args": None,
58         "roles": [],
59         "accepts": [],
60         "returns": [],
61     }
62     def export(method):
63         def args():
64             # Inspect method. Remove self from the argument list.
65             max_args = method.func_code.co_varnames[0:method.func_code.co_argcount]
66             defaults = method.func_defaults
67             if defaults is None:
68                 defaults = ()
69             min_args = max_args[0:len(max_args) - len(defaults)]
70
71             defaults = tuple([None for arg in min_args]) + defaults
72             return (min_args, max_args, defaults)
73
74         keywords['name'] = method.__name__
75         keywords['args'] = args
76         for arg in keywords:
77             method.__setattr__(arg, keywords[arg])
78
79         for arg in kwargs:
80             method.__setattr__(arg, kwargs[arg])
81         return method
82
83     return export
84
85
86 # status
87 # roles,
88 # accepts,
89 # returns
90
91 @export_to_docbook(roles=['self'], 
92                                    accepts=[], 
93                                    returns=Parameter([], 'A list of supported functions'))
94 @export_to_api(0)
95 def Help():
96     """Get a list of functions currently supported by the Node Manager API"""
97     return ''.join([method.__doc__ + '\n' for method in api_method_dict.itervalues()])
98
99 @export_to_docbook(roles=['self'], 
100                                    accepts=[Parameter(str, 'A ticket returned from GetSliceTicket()')], 
101                                    returns=Parameter(int, '1 if successful'))
102 @export_to_api(1)
103 def Ticket(ticket):
104     """The Node Manager periodically polls the PLC API for a list of all
105     slices that are allowed to exist on the given node. Before 
106     actions are performed on a delegated slice (such as creation),
107     a controller slice must deliver a valid slice ticket to NM. 
108     
109     This ticket is the value retured by PLC's GetSliceTicket() API call,
110     """
111     try:
112         data = ticket_module.verify(ticket)
113         name = data['slivers'][0]['name']
114         if data != None:
115             deliver_ticket(data)
116         logger.log('Ticket delivered for %s' % name)
117         Create(database.db.get(name))
118     except Exception, err:
119         raise xmlrpclib.Fault(102, 'Ticket error: ' + str(err))
120
121 @export_to_docbook(roles=['self'],
122                                    accepts=[], 
123                                    returns={'sliver_name' : Parameter(int, 'the associated xid')})
124 @export_to_api(0)
125 def GetXIDs():
126     """Return an dictionary mapping Slice names to XIDs"""
127     return dict([(pwent[0], pwent[2]) for pwent in pwd.getpwall() if pwent[6] == sliver_vs.Sliver_VS.SHELL])
128
129 @export_to_docbook(roles=['self'],
130                    accepts=[], 
131                                    returns={ 'sliver_name' : Parameter(str, 'the associated SSHKey')})
132 @export_to_api(0)
133 def GetSSHKeys():
134     """Return an dictionary mapping slice names to SSH keys"""
135     keydict = {}
136     for rec in database.db.itervalues():
137         if 'keys' in rec:
138             keydict[rec['name']] = rec['keys']
139     return keydict
140
141 @export_to_docbook(roles=['nm-controller', 'self'], 
142                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
143                                    returns=Parameter(int, '1 if successful'))
144 @export_to_api(1)
145 def Create(sliver_name):
146     """Create a non-PLC-instantiated sliver"""
147     rec = sliver_name
148     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_created(rec)
149
150 @export_to_docbook(roles=['nm-controller', 'self'], 
151                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
152                                    returns=Parameter(int, '1 if successful'))
153 @export_to_api(1)
154 def Destroy(sliver_name):
155     """Destroy a non-PLC-instantiated sliver"""
156     rec = sliver_name 
157     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_destroyed()
158
159 @export_to_docbook(roles=['nm-controller', 'self'], 
160                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
161                                    returns=Parameter(int, '1 if successful'))
162 @export_to_api(1)
163 def Start(sliver_name):
164     """Run start scripts belonging to the specified sliver"""
165     rec = sliver_name
166     accounts.get(rec['name']).start()
167
168 @export_to_docbook(roles=['nm-controller', 'self'], 
169                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
170                                    returns=Parameter(int, '1 if successful'))
171 @export_to_api(1)
172 def Stop(sliver_name):
173     """Kill all processes belonging to the specified sliver"""
174     rec = sliver_name
175     accounts.get(rec['name']).stop()
176
177 @export_to_docbook(roles=['nm-controller', 'self'], 
178                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
179                                    returns=Parameter(dict, "A resource specification"))
180 @export_to_api(1)
181 def GetEffectiveRSpec(sliver_name):
182     """Return the RSpec allocated to the specified sliver, including loans"""
183     rec = sliver_name
184     return rec.get('_rspec', {}).copy()
185
186 @export_to_docbook(roles=['nm-controller', 'self'], 
187                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
188                                     returns={
189                                                         "resource name" : Parameter(int, "amount")
190                                                 }
191                                   )
192 @export_to_api(1)
193 def GetRSpec(sliver_name):
194     """Return the RSpec allocated to the specified sliver, excluding loans"""
195     rec = sliver_name
196     return rec.get('rspec', {}).copy()
197
198 @export_to_docbook(roles=['nm-controller', 'self'], 
199                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
200                                         returns=[Mixed(Parameter(str, 'recipient slice name'),
201                                                      Parameter(str, 'resource name'),
202                                                      Parameter(int, 'resource amount')]) 
203                                   )
204 @export_to_api(1)
205 def GetLoans(sliver_name):
206     """Return the list of loans made by the specified sliver"""
207     rec = sliver_name
208     return rec.get('_loans', [])[:]
209
210 def validate_loans(obj):
211     """Check that <obj> is a valid loan specification."""
212     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
213     return type(obj)==list and False not in map(validate_loan, obj)
214
215 @export_to_docbook(roles=['nm-controller', 'self'], 
216                                 accepts=[ Parameter(str, 'A sliver/slice name.'),
217                                                   [Mixed(Parameter(str, 'recipient slice name'),
218                                                    Parameter(str, 'resource name'),
219                                                    Parameter(int, 'resource amount'))] ],
220                                 returns=Parameter(int, '1 if successful'))
221 @export_to_api(2)
222 def SetLoans(sliver_name, loans):
223     """Overwrite the list of loans made by the specified sliver.
224
225         Also, note that SetLoans will not throw an error if more capacity than the
226         RSpec is handed out, but it will silently discard those loans that would
227         put it over capacity.  This behavior may be replaced with error semantics
228         in the future.  As well, there is currently no asynchronous notification
229         of loss of resources.
230         """
231     rec = sliver_name
232     if not validate_loans(loans): raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
233     rec['_loans'] = loans
234     database.db.sync()