reguire gnupg1 on f>=31; sense the system to use gpg1 when installed
[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 xmlrpc.server
13 import socketserver
14 import errno
15 import os
16 import pwd
17 import socket
18 import struct
19 import threading
20 import xmlrpc.client
21 import slivermanager
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 import ticket as ticket_module
40 import tools
41
42 deliver_ticket = None  # set in slivermanager.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.__code__.co_varnames[0:method.__code__.co_argcount]
69             defaults = method.__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     names=list(api_method_dict.keys())
101     names.sort()
102     return ''.join(['**** ' + api_method_dict[name].__name__ + '\n' + api_method_dict[name].__doc__ + '\n'
103                     for name in names])
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     try:
117         data = ticket_module.verify(ticket)
118         name = data['slivers'][0]['name']
119         if data != None:
120             deliver_ticket(data)
121         logger.log('api_calls: Ticket delivered for %s' % name)
122         Create(database.db.get(name))
123     except Exception as err:
124         raise xmlrpc.client.Fault(102, 'Ticket error: ' + str(err))
125
126 @export_to_docbook(roles=['self'],
127                    accepts=[Parameter(str, 'A ticket returned from GetSlivers()')],
128                    returns=Parameter(int, '1 if successful'))
129 @export_to_api(1)
130 def AdminTicket(ticket):
131     """Admin interface to create slivers based on ticket returned by GetSlivers()."""
132     try:
133         data, = xmlrpc.client.loads(ticket)[0]
134         name = data['slivers'][0]['name']
135         if data != None:
136             deliver_ticket(data)
137         logger.log('api_calls: Admin Ticket delivered for %s' % name)
138         Create(database.db.get(name))
139     except Exception as err:
140         raise xmlrpc.client.Fault(102, 'Ticket error: ' + str(err))
141
142
143 @export_to_docbook(roles=['self'],
144                    accepts=[],
145                    returns={'sliver_name' : Parameter(int, 'the associated xid')})
146 @export_to_api(0)
147 def GetXIDs():
148     """Return an dictionary mapping Slice names to XIDs"""
149     return dict([(pwent[0], pwent[2]) for pwent in pwd.getpwall() if pwent[6] == slivermanager.sliver_password_shell])
150
151 @export_to_docbook(roles=['self'],
152                    accepts=[],
153                    returns={ 'sliver_name' : Parameter(str, 'the associated SSHKey')})
154 @export_to_api(0)
155 def GetSSHKeys():
156     """Return an dictionary mapping slice names to SSH keys"""
157     keydict = {}
158     for rec in database.db.values():
159         if 'keys' in rec:
160             keydict[rec['name']] = rec['keys']
161     return keydict
162
163
164 @export_to_docbook(roles=['nm-controller', 'self'],
165                     accepts=[Parameter(str, 'A sliver/slice name.')],
166                    returns=Parameter(int, '1 if successful'))
167 @export_to_api(1)
168 def Create(sliver_name):
169     """Create a non-PLC-instantiated sliver"""
170     rec = sliver_name
171     if rec['instantiation'] == 'delegated':
172         account.get(rec['name']).ensure_created(rec)
173         logger.log("api_calls: Create %s"%rec['name'])
174     else:
175         raise Exception("Only PLC can create non delegated slivers.")
176
177
178 @export_to_docbook(roles=['nm-controller', 'self'],
179                     accepts=[Parameter(str, 'A sliver/slice name.')],
180                    returns=Parameter(int, '1 if successful'))
181 @export_to_api(1)
182 def Destroy(sliver_name):
183     """Destroy a non-PLC-instantiated sliver"""
184     rec = sliver_name
185     if rec['instantiation'] == 'delegated':
186         account.get(rec['name']).ensure_destroyed()
187         logger.log("api_calls: Destroy %s"%rec['name'])
188     else:
189         raise Exception("Only PLC can destroy non delegated slivers.")
190
191
192 @export_to_docbook(roles=['nm-controller', 'self'],
193                     accepts=[Parameter(str, 'A sliver/slice name.')],
194                    returns=Parameter(int, '1 if successful'))
195 @export_to_api(1)
196 def Start(sliver_name):
197     """Configure and start sliver."""
198     rec = sliver_name
199     account.get(rec['name']).start(rec)
200     logger.log("api_calls: Start %s"%rec['name'])
201
202
203 @export_to_docbook(roles=['nm-controller', 'self'],
204                     accepts=[Parameter(str, 'A sliver/slice name.')],
205                    returns=Parameter(int, '1 if successful'))
206 @export_to_api(1)
207 def Stop(sliver_name):
208     """Kill all processes belonging to the specified sliver"""
209     rec = sliver_name
210     account.get(rec['name']).stop()
211     logger.log("api_calls: Stop %s"%rec['name'])
212
213
214 @export_to_docbook(roles=['nm-controller', 'self'],
215                     accepts=[Parameter(str, 'A sliver/slice name.')],
216                    returns=Parameter(int, '1 if successful'))
217 @export_to_api(1)
218 def ReCreate(sliver_name):
219     """Stop, Destroy, Create, Start sliver in order to reinstall it."""
220     rec = sliver_name
221     account.get(rec['name']).stop()
222     account.get(rec['name']).ensure_created(rec)
223     account.get(rec['name']).start(rec)
224     logger.log("api_calls: ReCreate %s"%rec['name'])
225
226 @export_to_docbook(roles=['nm-controller', 'self'],
227                     accepts=[Parameter(str, 'A sliver/slice name.')],
228                    returns=Parameter(dict, "A resource specification"))
229 @export_to_api(1)
230 def GetEffectiveRSpec(sliver_name):
231     """Return the RSpec allocated to the specified sliver, including loans"""
232     rec = sliver_name
233     return rec.get('_rspec', {}).copy()
234
235
236 @export_to_docbook(roles=['nm-controller', 'self'],
237                     accepts=[Parameter(str, 'A sliver/slice name.')],
238                     returns={"resource name" : Parameter(int, "amount")})
239 @export_to_api(1)
240 def GetRSpec(sliver_name):
241     """Return the RSpec allocated to the specified sliver, excluding loans"""
242     rec = sliver_name
243     return rec.get('rspec', {}).copy()
244
245
246 @export_to_docbook(roles=['nm-controller', 'self'],
247                     accepts=[Parameter(str, 'A sliver/slice name.')],
248                     returns=[Mixed(Parameter(str, 'recipient slice name'),
249                              Parameter(str, 'resource name'),
250                              Parameter(int, 'resource amount'))])
251
252 @export_to_api(1)
253 def GetLoans(sliver_name):
254     """Return the list of loans made by the specified sliver"""
255     rec = sliver_name
256     return rec.get('_loans', [])[:]
257
258 def validate_loans(loans):
259     """Check that <obj> is a list of valid loan specifications."""
260     def validate_loan(loan):
261         return (type(loan)==list or type(loan)==tuple) and len(loan)==3 \
262             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
263     return type(loans)==list and False not in [validate_loan(load) for loan in loans]
264
265
266 @export_to_docbook(roles=['nm-controller', 'self'],
267                    accepts=[ Parameter(str, 'A sliver/slice name.'),
268                              [Mixed(Parameter(str, 'recipient slice name'),
269                                     Parameter(str, 'resource name'),
270                                     Parameter(int, 'resource amount'))], ],
271                    returns=Parameter(int, '1 if successful'))
272 @export_to_api(2)
273 def SetLoans(sliver_name, loans):
274     """Overwrite the list of loans made by the specified sliver.
275
276     Also, note that SetLoans will not throw an error if more capacity than the
277     RSpec is handed out, but it will silently discard those loans that would
278     put it over capacity.  This behavior may be replaced with error semantics
279     in the future.  As well, there is currently no asynchronous notification
280     of loss of resources."""
281     rec = sliver_name
282     if not validate_loans(loans):
283         raise xmlrpc.client.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
284     rec['_loans'] = loans
285     database.db.sync()
286
287 @export_to_docbook(roles=['nm-controller', 'self'],
288                    returns=Parameter(dict, 'Record dictionary'))
289 @export_to_api(0)
290 def GetRecord(sliver_name):
291     """Return sliver record"""
292     rec = sliver_name
293     return rec