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