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