- Change .py files to use 4-space indents and no hard tab characters.
[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.
37 # A better approach will involve more extensive code 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 slivermanager.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     names=api_method_dict.keys()
104     names.sort()
105     return ''.join(['**** ' + api_method_dict[name].__name__ + '\n' + api_method_dict[name].__doc__ + '\n'
106                     for name in names])
107
108 @export_to_docbook(roles=['self'],
109                    accepts=[Parameter(str, 'A ticket returned from GetSliceTicket()')],
110                    returns=Parameter(int, '1 if successful'))
111 @export_to_api(1)
112 def Ticket(ticket):
113     """The Node Manager periodically polls the PLC API for a list of all
114     slices that are allowed to exist on the given node. Before
115     actions are performed on a delegated slice (such as creation),
116     a controller slice must deliver a valid slice ticket to NM.
117
118     This ticket is the value retured by PLC's GetSliceTicket() API call."""
119     try:
120         data = ticket_module.verify(ticket)
121         name = data['slivers'][0]['name']
122         if data != None:
123             deliver_ticket(data)
124         logger.log('api_calls: Ticket delivered for %s' % name)
125         Create(database.db.get(name))
126     except Exception, err:
127         raise xmlrpclib.Fault(102, 'Ticket error: ' + str(err))
128
129 @export_to_docbook(roles=['self'],
130                    accepts=[Parameter(str, 'A ticket returned from GetSlivers()')],
131                    returns=Parameter(int, '1 if successful'))
132 @export_to_api(1)
133 def AdminTicket(ticket):
134     """Admin interface to create slivers based on ticket returned by GetSlivers()."""
135     try:
136         data, = xmlrpclib.loads(ticket)[0]
137         name = data['slivers'][0]['name']
138         if data != None:
139             deliver_ticket(data)
140         logger.log('api_calls: Admin Ticket delivered for %s' % name)
141         Create(database.db.get(name))
142     except Exception, err:
143         raise xmlrpclib.Fault(102, 'Ticket error: ' + str(err))
144
145
146 @export_to_docbook(roles=['self'],
147                    accepts=[],
148                    returns={'sliver_name' : Parameter(int, 'the associated xid')})
149 @export_to_api(0)
150 def GetXIDs():
151     """Return an dictionary mapping Slice names to XIDs"""
152     return dict([(pwent[0], pwent[2]) for pwent in pwd.getpwall() if pwent[6] == sliver_vs.Sliver_VS.SHELL])
153
154 @export_to_docbook(roles=['self'],
155                    accepts=[],
156                    returns={ 'sliver_name' : Parameter(str, 'the associated SSHKey')})
157 @export_to_api(0)
158 def GetSSHKeys():
159     """Return an dictionary mapping slice names to SSH keys"""
160     keydict = {}
161     for rec in database.db.itervalues():
162         if 'keys' in rec:
163             keydict[rec['name']] = rec['keys']
164     return keydict
165
166
167 @export_to_docbook(roles=['nm-controller', 'self'],
168                     accepts=[Parameter(str, 'A sliver/slice name.')],
169                    returns=Parameter(int, '1 if successful'))
170 @export_to_api(1)
171 def Create(sliver_name):
172     """Create a non-PLC-instantiated sliver"""
173     rec = sliver_name
174     if rec['instantiation'] == 'delegated':
175         accounts.get(rec['name']).ensure_created(rec)
176         logger.log("api_calls: Create %s"%rec['name'])
177     else:
178         raise Exception, "Only PLC can create non delegated slivers."
179
180
181 @export_to_docbook(roles=['nm-controller', 'self'],
182                     accepts=[Parameter(str, 'A sliver/slice name.')],
183                    returns=Parameter(int, '1 if successful'))
184 @export_to_api(1)
185 def Destroy(sliver_name):
186     """Destroy a non-PLC-instantiated sliver"""
187     rec = sliver_name
188     if rec['instantiation'] == 'delegated':
189         accounts.get(rec['name']).ensure_destroyed()
190         logger.log("api_calls: Destroy %s"%rec['name'])
191     else:
192         raise Exception, "Only PLC can destroy non delegated slivers."
193
194
195 @export_to_docbook(roles=['nm-controller', 'self'],
196                     accepts=[Parameter(str, 'A sliver/slice name.')],
197                    returns=Parameter(int, '1 if successful'))
198 @export_to_api(1)
199 def Start(sliver_name):
200     """Configure and start sliver."""
201     rec = sliver_name
202     accounts.get(rec['name']).start(rec)
203     logger.log("api_calls: Start %s"%rec['name'])
204
205
206 @export_to_docbook(roles=['nm-controller', 'self'],
207                     accepts=[Parameter(str, 'A sliver/slice name.')],
208                    returns=Parameter(int, '1 if successful'))
209 @export_to_api(1)
210 def Stop(sliver_name):
211     """Kill all processes belonging to the specified sliver"""
212     rec = sliver_name
213     accounts.get(rec['name']).stop()
214     logger.log("api_calls: Stop %s"%rec['name'])
215
216
217 @export_to_docbook(roles=['nm-controller', 'self'],
218                     accepts=[Parameter(str, 'A sliver/slice name.')],
219                    returns=Parameter(int, '1 if successful'))
220 @export_to_api(1)
221 def ReCreate(sliver_name):
222     """Stop, Destroy, Create, Start sliver in order to reinstall it."""
223     rec = sliver_name
224     accounts.get(rec['name']).stop()
225     accounts.get(rec['name']).ensure_created(rec)
226     accounts.get(rec['name']).start(rec)
227     logger.log("api_calls: ReCreate %s"%rec['name'])
228
229 @export_to_docbook(roles=['nm-controller', 'self'],
230                     accepts=[Parameter(str, 'A sliver/slice name.')],
231                    returns=Parameter(dict, "A resource specification"))
232 @export_to_api(1)
233 def GetEffectiveRSpec(sliver_name):
234     """Return the RSpec allocated to the specified sliver, including loans"""
235     rec = sliver_name
236     return rec.get('_rspec', {}).copy()
237
238
239 @export_to_docbook(roles=['nm-controller', 'self'],
240                     accepts=[Parameter(str, 'A sliver/slice name.')],
241                     returns={"resource name" : Parameter(int, "amount")})
242 @export_to_api(1)
243 def GetRSpec(sliver_name):
244     """Return the RSpec allocated to the specified sliver, excluding loans"""
245     rec = sliver_name
246     return rec.get('rspec', {}).copy()
247
248
249 @export_to_docbook(roles=['nm-controller', 'self'],
250                     accepts=[Parameter(str, 'A sliver/slice name.')],
251                     returns=[Mixed(Parameter(str, 'recipient slice name'),
252                              Parameter(str, 'resource name'),
253                              Parameter(int, 'resource amount'))])
254
255 @export_to_api(1)
256 def GetLoans(sliver_name):
257     """Return the list of loans made by the specified sliver"""
258     rec = sliver_name
259     return rec.get('_loans', [])[:]
260
261 def validate_loans(loans):
262     """Check that <obj> is a list of valid loan specifications."""
263     def validate_loan(loan):
264         return (type(loan)==list or type(loan)==tuple) and len(loan)==3 \
265             and type(loan[0])==str and type(loan[1])==str and loan[1] in database.LOANABLE_RESOURCES and type(loan[2])==int and loan[2]>=0
266     return type(loans)==list and False not in [validate_loan(load) for loan in loans]
267
268
269 @export_to_docbook(roles=['nm-controller', 'self'],
270                    accepts=[ Parameter(str, 'A sliver/slice name.'),
271                              [Mixed(Parameter(str, 'recipient slice name'),
272                                     Parameter(str, 'resource name'),
273                                     Parameter(int, 'resource amount'))], ],
274                    returns=Parameter(int, '1 if successful'))
275 @export_to_api(2)
276 def SetLoans(sliver_name, loans):
277     """Overwrite the list of loans made by the specified sliver.
278
279     Also, note that SetLoans will not throw an error if more capacity than the
280     RSpec is handed out, but it will silently discard those loans that would
281     put it over capacity.  This behavior may be replaced with error semantics
282     in the future.  As well, there is currently no asynchronous notification
283     of loss of resources."""
284     rec = sliver_name
285     if not validate_loans(loans):
286         raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
287     rec['_loans'] = loans
288     database.db.sync()
289
290 @export_to_docbook(roles=['nm-controller', 'self'],
291                    returns=Parameter(dict, 'Record dictionary'))
292 @export_to_api(0)
293 def GetRecord(sliver_name):
294     """Return sliver record"""
295     rec = sliver_name
296     return rec