svn:keywords
[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 extrac 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('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('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': accounts.get(rec['name']).ensure_created(rec)
174     else: raise Exception, "Only PLC can create non delegated slivers."
175
176
177 @export_to_docbook(roles=['nm-controller', 'self'], 
178                     accepts=[Parameter(str, 'A sliver/slice name.')], 
179                    returns=Parameter(int, '1 if successful'))
180 @export_to_api(1)
181 def Destroy(sliver_name):
182     """Destroy a non-PLC-instantiated sliver"""
183     rec = sliver_name 
184     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_destroyed()
185     else: raise Exception, "Only PLC can destroy non delegated slivers."
186
187
188 @export_to_docbook(roles=['nm-controller', 'self'], 
189                     accepts=[Parameter(str, 'A sliver/slice name.')], 
190                    returns=Parameter(int, '1 if successful'))
191 @export_to_api(1)
192 def Start(sliver_name):
193     """Configure and start sliver."""
194     rec = sliver_name
195     accounts.get(rec['name']).start(rec)
196
197
198 @export_to_docbook(roles=['nm-controller', 'self'], 
199                     accepts=[Parameter(str, 'A sliver/slice name.')], 
200                    returns=Parameter(int, '1 if successful'))
201 @export_to_api(1)
202 def Stop(sliver_name):
203     """Kill all processes belonging to the specified sliver"""
204     rec = sliver_name
205     accounts.get(rec['name']).stop()
206
207
208 @export_to_docbook(roles=['nm-controller', 'self'], 
209                     accepts=[Parameter(str, 'A sliver/slice name.')], 
210                    returns=Parameter(int, '1 if successful'))
211 @export_to_api(1)
212 def ReCreate(sliver_name):
213     """Stop, Destroy, Create, Start sliver in order to reinstall it."""
214     rec = sliver_name
215     accounts.get(rec['name']).stop()
216     accounts.get(rec['name']).ensure_created(rec)
217     accounts.get(rec['name']).start(rec)
218
219
220 @export_to_docbook(roles=['nm-controller', 'self'], 
221                     accepts=[Parameter(str, 'A sliver/slice name.')], 
222                    returns=Parameter(dict, "A resource specification"))
223 @export_to_api(1)
224 def GetEffectiveRSpec(sliver_name):
225     """Return the RSpec allocated to the specified sliver, including loans"""
226     rec = sliver_name
227     return rec.get('_rspec', {}).copy()
228
229
230 @export_to_docbook(roles=['nm-controller', 'self'], 
231                     accepts=[Parameter(str, 'A sliver/slice name.')], 
232                     returns={"resource name" : Parameter(int, "amount")})
233 @export_to_api(1)
234 def GetRSpec(sliver_name):
235     """Return the RSpec allocated to the specified sliver, excluding loans"""
236     rec = sliver_name
237     return rec.get('rspec', {}).copy()
238
239
240 @export_to_docbook(roles=['nm-controller', 'self'], 
241                     accepts=[Parameter(str, 'A sliver/slice name.')], 
242                     returns=[Mixed(Parameter(str, 'recipient slice name'),
243                              Parameter(str, 'resource name'),
244                              Parameter(int, 'resource amount'))])
245
246 @export_to_api(1)
247 def GetLoans(sliver_name):
248     """Return the list of loans made by the specified sliver"""
249     rec = sliver_name
250     return rec.get('_loans', [])[:]
251
252 def validate_loans(obj):
253     """Check that <obj> is a valid loan specification."""
254     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
255     return type(obj)==list and False not in map(validate_loan, obj)
256
257 @export_to_docbook(roles=['nm-controller', 'self'], 
258                 accepts=[ Parameter(str, 'A sliver/slice name.'),
259                           [Mixed(Parameter(str, 'recipient slice name'),
260                            Parameter(str, 'resource name'),
261                            Parameter(int, 'resource amount'))] ],
262                 returns=Parameter(int, '1 if successful'))
263 @export_to_api(2)
264 def SetLoans(sliver_name, loans):
265     """Overwrite the list of loans made by the specified sliver.
266
267     Also, note that SetLoans will not throw an error if more capacity than the
268     RSpec is handed out, but it will silently discard those loans that would
269     put it over capacity.  This behavior may be replaced with error semantics
270     in the future.  As well, there is currently no asynchronous notification
271     of loss of resources.
272     """
273     rec = sliver_name
274     if not validate_loans(loans): raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
275     rec['_loans'] = loans
276     database.db.sync()
277
278 @export_to_docbook(roles=['nm-controller', 'self'], 
279                          returns=Parameter(dict, 'Record dictionary'))
280 @export_to_api(0)
281 def GetRecord(sliver_name):
282     """Return sliver record"""
283     rec = sliver_name
284     return rec