incorporate changelog from 2.0
[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 import sliver_lxc
22
23 try:
24     from PLC.Parameter import Parameter, Mixed
25 except:
26     def Parameter(a = None, b = None): pass
27     def Mixed(a = None, b = None, c = None): pass
28
29
30 import account
31 import logger
32
33 # TODO: These try/excepts are a hack to allow doc/DocBookLocal.py to
34 # import this file in order to extract the documentation from each
35 # exported function.
36 # A better approach will involve more extensive code splitting, I think.
37 try: import database
38 except: import logger as database
39 #try: import sliver_vs
40 #except: import logger as sliver_vs
41 import ticket as ticket_module
42 import tools
43
44 deliver_ticket = None  # set in slivermanager.start()
45
46 api_method_dict = {}
47 nargs_dict = {}
48
49 def export_to_api(nargs):
50     def export(method):
51         nargs_dict[method.__name__] = nargs
52         api_method_dict[method.__name__] = method
53         return method
54     return export
55
56 def export_to_docbook(**kwargs):
57
58     keywords = {
59         "group" : "NMAPI",
60         "status" : "current",
61         "name": None,
62         "args": None,
63         "roles": [],
64         "accepts": [],
65         "returns": [],
66     }
67     def export(method):
68         def args():
69             # Inspect method. Remove self from the argument list.
70             max_args = method.func_code.co_varnames[0:method.func_code.co_argcount]
71             defaults = method.func_defaults
72             if defaults is None:
73                 defaults = ()
74             min_args = max_args[0:len(max_args) - len(defaults)]
75
76             defaults = tuple([None for arg in min_args]) + defaults
77             return (min_args, max_args, defaults)
78
79         keywords['name'] = method.__name__
80         keywords['args'] = args
81         for arg in keywords:
82             method.__setattr__(arg, keywords[arg])
83
84         for arg in kwargs:
85             method.__setattr__(arg, kwargs[arg])
86         return method
87
88     return export
89
90
91 # status
92 # roles,
93 # accepts,
94 # returns
95
96 @export_to_docbook(roles=['self'],
97                    accepts=[],
98                    returns=Parameter([], 'A list of supported functions'))
99 @export_to_api(0)
100 def Help():
101     """Get a list of functions currently supported by the Node Manager API"""
102     names=api_method_dict.keys()
103     names.sort()
104     return ''.join(['**** ' + api_method_dict[name].__name__ + '\n' + api_method_dict[name].__doc__ + '\n'
105                     for name in names])
106
107 @export_to_docbook(roles=['self'],
108                    accepts=[Parameter(str, 'A ticket returned from GetSliceTicket()')],
109                    returns=Parameter(int, '1 if successful'))
110 @export_to_api(1)
111 def Ticket(ticket):
112     """The Node Manager periodically polls the PLC API for a list of all
113     slices that are allowed to exist on the given node. Before
114     actions are performed on a delegated slice (such as creation),
115     a controller slice must deliver a valid slice ticket to NM.
116
117     This ticket is the value retured by PLC's GetSliceTicket() API call."""
118     try:
119         data = ticket_module.verify(ticket)
120         name = data['slivers'][0]['name']
121         if data != None:
122             deliver_ticket(data)
123         logger.log('api_calls: Ticket delivered for %s' % name)
124         Create(database.db.get(name))
125     except Exception, err:
126         raise xmlrpclib.Fault(102, 'Ticket error: ' + str(err))
127
128 @export_to_docbook(roles=['self'],
129                    accepts=[Parameter(str, 'A ticket returned from GetSlivers()')],
130                    returns=Parameter(int, '1 if successful'))
131 @export_to_api(1)
132 def AdminTicket(ticket):
133     """Admin interface to create slivers based on ticket returned by GetSlivers()."""
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_lxc.Sliver_LXC.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         account.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         account.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     account.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     account.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     account.get(rec['name']).stop()
224     account.get(rec['name']).ensure_created(rec)
225     account.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(loans):
261     """Check that <obj> is a list of valid loan specifications."""
262     def validate_loan(loan):
263         return (type(loan)==list or type(loan)==tuple) and len(loan)==3 \
264             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
265     return type(loans)==list and False not in [validate_loan(load) for loan in loans]
266
267
268 @export_to_docbook(roles=['nm-controller', 'self'],
269                    accepts=[ Parameter(str, 'A sliver/slice name.'),
270                              [Mixed(Parameter(str, 'recipient slice name'),
271                                     Parameter(str, 'resource name'),
272                                     Parameter(int, 'resource amount'))], ],
273                    returns=Parameter(int, '1 if successful'))
274 @export_to_api(2)
275 def SetLoans(sliver_name, loans):
276     """Overwrite the list of loans made by the specified sliver.
277
278     Also, note that SetLoans will not throw an error if more capacity than the
279     RSpec is handed out, but it will silently discard those loans that would
280     put it over capacity.  This behavior may be replaced with error semantics
281     in the future.  As well, there is currently no asynchronous notification
282     of loss of resources."""
283     rec = sliver_name
284     if not validate_loans(loans):
285         raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
286     rec['_loans'] = loans
287     database.db.sync()
288
289 @export_to_docbook(roles=['nm-controller', 'self'],
290                    returns=Parameter(dict, 'Record dictionary'))
291 @export_to_api(0)
292 def GetRecord(sliver_name):
293     """Return sliver record"""
294     rec = sliver_name
295     return rec