expects the 'interfaces' key in GetSlivers - review logs to always mention module
[nodemanager.git] / api_calls.py
1 # $Id$
2 # $URL$
3
4 """Sliver manager API.
5
6 This module exposes an XMLRPC interface that allows PlanetLab users to
7 create/destroy slivers with delegated instantiation, start and stop
8 slivers, make resource loans, and examine resource allocations.  The
9 XMLRPC is provided on a localhost-only TCP port as well as via a Unix
10 domain socket that is accessible by ssh-ing into a delegate account
11 with the forward_api_calls shell.
12 """
13
14 import SimpleXMLRPCServer
15 import SocketServer
16 import errno
17 import os
18 import pwd
19 import socket
20 import struct
21 import threading
22 import xmlrpclib
23
24 try:
25     from PLC.Parameter import Parameter, Mixed
26 except:
27     def Parameter(a = None, b = None): pass
28     def Mixed(a = None, b = None, c = None): pass
29
30
31 import accounts
32 import logger
33
34 # TODO: These try/excepts are a hack to allow doc/DocBookLocal.py to 
35 # import this file in order to extract the documentation from each 
36 # exported function.  A better approach will involve more extensive code
37 # splitting, I think.
38 try: import database
39 except: import logger as database
40 try: import sliver_vs
41 except: import logger as sliver_vs
42 import ticket as ticket_module
43 import tools
44
45 deliver_ticket = None  # set in sm.py:start()
46
47 api_method_dict = {}
48 nargs_dict = {}
49
50 def export_to_api(nargs):
51     def export(method):
52         nargs_dict[method.__name__] = nargs
53         api_method_dict[method.__name__] = method
54         return method
55     return export
56
57 def export_to_docbook(**kwargs):
58
59     keywords = {
60         "group" : "NMAPI",
61         "status" : "current",
62         "name": None,
63         "args": None,
64         "roles": [],
65         "accepts": [],
66         "returns": [],
67     }
68     def export(method):
69         def args():
70             # Inspect method. Remove self from the argument list.
71             max_args = method.func_code.co_varnames[0:method.func_code.co_argcount]
72             defaults = method.func_defaults
73             if defaults is None:
74                 defaults = ()
75             min_args = max_args[0:len(max_args) - len(defaults)]
76
77             defaults = tuple([None for arg in min_args]) + defaults
78             return (min_args, max_args, defaults)
79
80         keywords['name'] = method.__name__
81         keywords['args'] = args
82         for arg in keywords:
83             method.__setattr__(arg, keywords[arg])
84
85         for arg in kwargs:
86             method.__setattr__(arg, kwargs[arg])
87         return method
88
89     return export
90
91
92 # status
93 # roles,
94 # accepts,
95 # returns
96
97 @export_to_docbook(roles=['self'], 
98                    accepts=[], 
99                    returns=Parameter([], 'A list of supported functions'))
100 @export_to_api(0)
101 def Help():
102     """Get a list of functions currently supported by the Node Manager API"""
103     return ''.join([method.__doc__ + '\n' for method in api_method_dict.itervalues()])
104
105 @export_to_docbook(roles=['self'], 
106                    accepts=[Parameter(str, 'A ticket returned from GetSliceTicket()')], 
107                    returns=Parameter(int, '1 if successful'))
108 @export_to_api(1)
109 def Ticket(ticket):
110     """The Node Manager periodically polls the PLC API for a list of all
111     slices that are allowed to exist on the given node. Before 
112     actions are performed on a delegated slice (such as creation),
113     a controller slice must deliver a valid slice ticket to NM. 
114     
115     This ticket is the value retured by PLC's GetSliceTicket() API call,
116     """
117     try:
118         data = ticket_module.verify(ticket)
119         name = data['slivers'][0]['name']
120         if data != None:
121             deliver_ticket(data)
122         logger.log('api_calls: Ticket delivered for %s' % name)
123         Create(database.db.get(name))
124     except Exception, err:
125         raise xmlrpclib.Fault(102, 'Ticket error: ' + str(err))
126
127 @export_to_docbook(roles=['self'], 
128                    accepts=[Parameter(str, 'A ticket returned from GetSlivers()')], 
129                    returns=Parameter(int, '1 if successful'))
130 @export_to_api(1)
131 def AdminTicket(ticket):
132     """Admin interface to create slivers based on ticket returned by GetSlivers().
133     """
134     try:
135         data, = xmlrpclib.loads(ticket)[0]
136         name = data['slivers'][0]['name']
137         if data != None:
138             deliver_ticket(data)
139         logger.log('api_calls: Admin Ticket delivered for %s' % name)
140         Create(database.db.get(name))
141     except Exception, err:
142         raise xmlrpclib.Fault(102, 'Ticket error: ' + str(err))
143
144
145 @export_to_docbook(roles=['self'],
146                    accepts=[], 
147                    returns={'sliver_name' : Parameter(int, 'the associated xid')})
148 @export_to_api(0)
149 def GetXIDs():
150     """Return an dictionary mapping Slice names to XIDs"""
151     return dict([(pwent[0], pwent[2]) for pwent in pwd.getpwall() if pwent[6] == sliver_vs.Sliver_VS.SHELL])
152
153 @export_to_docbook(roles=['self'],
154                    accepts=[], 
155                    returns={ 'sliver_name' : Parameter(str, 'the associated SSHKey')})
156 @export_to_api(0)
157 def GetSSHKeys():
158     """Return an dictionary mapping slice names to SSH keys"""
159     keydict = {}
160     for rec in database.db.itervalues():
161         if 'keys' in rec:
162             keydict[rec['name']] = rec['keys']
163     return keydict
164
165
166 @export_to_docbook(roles=['nm-controller', 'self'], 
167                     accepts=[Parameter(str, 'A sliver/slice name.')], 
168                    returns=Parameter(int, '1 if successful'))
169 @export_to_api(1)
170 def Create(sliver_name):
171     """Create a non-PLC-instantiated sliver"""
172     rec = sliver_name
173     if rec['instantiation'] == 'delegated': 
174         accounts.get(rec['name']).ensure_created(rec)
175         logger.log("api_calls: Create %s"%rec['name'])
176     else: 
177         raise Exception, "Only PLC can create non delegated slivers."
178
179
180 @export_to_docbook(roles=['nm-controller', 'self'], 
181                     accepts=[Parameter(str, 'A sliver/slice name.')], 
182                    returns=Parameter(int, '1 if successful'))
183 @export_to_api(1)
184 def Destroy(sliver_name):
185     """Destroy a non-PLC-instantiated sliver"""
186     rec = sliver_name 
187     if rec['instantiation'] == 'delegated': 
188         accounts.get(rec['name']).ensure_destroyed()
189         logger.log("api_calls: Destroy %s"%rec['name'])
190     else: 
191         raise Exception, "Only PLC can destroy non delegated slivers."
192
193
194 @export_to_docbook(roles=['nm-controller', 'self'], 
195                     accepts=[Parameter(str, 'A sliver/slice name.')], 
196                    returns=Parameter(int, '1 if successful'))
197 @export_to_api(1)
198 def Start(sliver_name):
199     """Configure and start sliver."""
200     rec = sliver_name
201     accounts.get(rec['name']).start(rec)
202     logger.log("api_calls: Start %s"%rec['name'])
203
204
205 @export_to_docbook(roles=['nm-controller', 'self'], 
206                     accepts=[Parameter(str, 'A sliver/slice name.')], 
207                    returns=Parameter(int, '1 if successful'))
208 @export_to_api(1)
209 def Stop(sliver_name):
210     """Kill all processes belonging to the specified sliver"""
211     rec = sliver_name
212     accounts.get(rec['name']).stop()
213     logger.log("api_calls: Stop %s"%rec['name'])
214
215
216 @export_to_docbook(roles=['nm-controller', 'self'], 
217                     accepts=[Parameter(str, 'A sliver/slice name.')], 
218                    returns=Parameter(int, '1 if successful'))
219 @export_to_api(1)
220 def ReCreate(sliver_name):
221     """Stop, Destroy, Create, Start sliver in order to reinstall it."""
222     rec = sliver_name
223     accounts.get(rec['name']).stop()
224     accounts.get(rec['name']).ensure_created(rec)
225     accounts.get(rec['name']).start(rec)
226     logger.log("api_calls: ReCreate %s"%rec['name'])
227
228 @export_to_docbook(roles=['nm-controller', 'self'], 
229                     accepts=[Parameter(str, 'A sliver/slice name.')], 
230                    returns=Parameter(dict, "A resource specification"))
231 @export_to_api(1)
232 def GetEffectiveRSpec(sliver_name):
233     """Return the RSpec allocated to the specified sliver, including loans"""
234     rec = sliver_name
235     return rec.get('_rspec', {}).copy()
236
237
238 @export_to_docbook(roles=['nm-controller', 'self'], 
239                     accepts=[Parameter(str, 'A sliver/slice name.')], 
240                     returns={"resource name" : Parameter(int, "amount")})
241 @export_to_api(1)
242 def GetRSpec(sliver_name):
243     """Return the RSpec allocated to the specified sliver, excluding loans"""
244     rec = sliver_name
245     return rec.get('rspec', {}).copy()
246
247
248 @export_to_docbook(roles=['nm-controller', 'self'], 
249                     accepts=[Parameter(str, 'A sliver/slice name.')], 
250                     returns=[Mixed(Parameter(str, 'recipient slice name'),
251                              Parameter(str, 'resource name'),
252                              Parameter(int, 'resource amount'))])
253
254 @export_to_api(1)
255 def GetLoans(sliver_name):
256     """Return the list of loans made by the specified sliver"""
257     rec = sliver_name
258     return rec.get('_loans', [])[:]
259
260 def validate_loans(obj):
261     """Check that <obj> is a valid loan specification."""
262     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
263     return type(obj)==list and False not in map(validate_loan, obj)
264
265 @export_to_docbook(roles=['nm-controller', 'self'], 
266                 accepts=[ Parameter(str, 'A sliver/slice name.'),
267                           [Mixed(Parameter(str, 'recipient slice name'),
268                            Parameter(str, 'resource name'),
269                            Parameter(int, 'resource amount'))] ],
270                 returns=Parameter(int, '1 if successful'))
271 @export_to_api(2)
272 def SetLoans(sliver_name, loans):
273     """Overwrite the list of loans made by the specified sliver.
274
275     Also, note that SetLoans will not throw an error if more capacity than the
276     RSpec is handed out, but it will silently discard those loans that would
277     put it over capacity.  This behavior may be replaced with error semantics
278     in the future.  As well, there is currently no asynchronous notification
279     of loss of resources.
280     """
281     rec = sliver_name
282     if not validate_loans(loans): raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
283     rec['_loans'] = loans
284     database.db.sync()
285
286 @export_to_docbook(roles=['nm-controller', 'self'], 
287                          returns=Parameter(dict, 'Record dictionary'))
288 @export_to_api(0)
289 def GetRecord(sliver_name):
290     """Return sliver record"""
291     rec = sliver_name
292     return rec