In case the API isn't installed, skip docbook stuffs.
[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
43 api_method_dict = {}
44 nargs_dict = {}
45
46 def export_to_api(nargs):
47     def export(method):
48         nargs_dict[method.__name__] = nargs
49         api_method_dict[method.__name__] = method
50         return method
51     return export
52
53 def export_to_docbook(**kwargs):
54
55     keywords = {
56         "group" : "NMAPI",
57         "status" : "current",
58         "name": None,
59         "args": None,
60         "roles": [],
61         "accepts": [],
62         "returns": [],
63     }
64     def export(method):
65         def args():
66             # Inspect method. Remove self from the argument list.
67             max_args = method.func_code.co_varnames[0:method.func_code.co_argcount]
68             defaults = method.func_defaults
69             if defaults is None:
70                 defaults = ()
71             min_args = max_args[0:len(max_args) - len(defaults)]
72
73             defaults = tuple([None for arg in min_args]) + defaults
74             return (min_args, max_args, defaults)
75
76         keywords['name'] = method.__name__
77         keywords['args'] = args
78         for arg in keywords:
79             method.__setattr__(arg, keywords[arg])
80
81         for arg in kwargs:
82             method.__setattr__(arg, kwargs[arg])
83         return method
84
85     return export
86
87
88 # status
89 # roles,
90 # accepts,
91 # returns
92
93 @export_to_docbook(roles=['self'], 
94                                    accepts=[], 
95                                    returns=Parameter([], 'A list of supported functions'))
96 @export_to_api(0)
97 def Help():
98     """Get a list of functions currently supported by the Node Manager API"""
99     return ''.join([method.__doc__ + '\n' for method in api_method_dict.itervalues()])
100
101 @export_to_docbook(roles=['self'], 
102                                    accepts=[Parameter(str, 'A ticket returned from GetSliceTicket()')], 
103                                    returns=Parameter(int, '1 if successful'))
104 @export_to_api(1)
105 def Ticket(ticket):
106     """The Node Manager periodically polls the PLC API for a list of all
107     slices that are allowed to exist on the given node. Before 
108     actions are performed on a delegated slice (such as creation),
109     a controller slice must deliver a valid slice ticket to NM. 
110     
111     This ticket is the value retured by PLC's GetSliceTicket() API call,
112     """
113     try:
114         data = ticket_module.verify(ticket)
115         name = data['slivers'][0]['name']
116         if data != None:
117             deliver_ticket(data)
118         logger.log('Ticket delivered for %s' % name)
119         Create(database.db.get(name))
120     except Exception, err:
121         raise xmlrpclib.Fault(102, 'Ticket error: ' + str(err))
122
123 @export_to_docbook(roles=['self'],
124                                    accepts=[], 
125                                    returns={'sliver_name' : Parameter(int, 'the associated xid')})
126 @export_to_api(0)
127 def GetXIDs():
128     """Return an dictionary mapping Slice names to XIDs"""
129     return dict([(pwent[0], pwent[2]) for pwent in pwd.getpwall() if pwent[6] == sliver_vs.Sliver_VS.SHELL])
130
131 @export_to_docbook(roles=['self'],
132                    accepts=[], 
133                                    returns={ 'sliver_name' : Parameter(str, 'the associated SSHKey')})
134 @export_to_api(0)
135 def GetSSHKeys():
136     """Return an dictionary mapping slice names to SSH keys"""
137     keydict = {}
138     for rec in database.db.itervalues():
139         if 'keys' in rec:
140             keydict[rec['name']] = rec['keys']
141     return keydict
142
143 @export_to_docbook(roles=['nm-controller', 'self'], 
144                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
145                                    returns=Parameter(int, '1 if successful'))
146 @export_to_api(1)
147 def Create(sliver_name):
148     """Create a non-PLC-instantiated sliver"""
149     rec = sliver_name
150     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_created(rec)
151
152 @export_to_docbook(roles=['nm-controller', 'self'], 
153                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
154                                    returns=Parameter(int, '1 if successful'))
155 @export_to_api(1)
156 def Destroy(sliver_name):
157     """Destroy a non-PLC-instantiated sliver"""
158     rec = sliver_name 
159     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_destroyed()
160
161 @export_to_docbook(roles=['nm-controller', 'self'], 
162                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
163                                    returns=Parameter(int, '1 if successful'))
164 @export_to_api(1)
165 def Start(sliver_name):
166     """Run start scripts belonging to the specified sliver"""
167     rec = sliver_name
168     accounts.get(rec['name']).start()
169
170 @export_to_docbook(roles=['nm-controller', 'self'], 
171                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
172                                    returns=Parameter(int, '1 if successful'))
173 @export_to_api(1)
174 def Stop(sliver_name):
175     """Kill all processes belonging to the specified sliver"""
176     rec = sliver_name
177     accounts.get(rec['name']).stop()
178
179 @export_to_docbook(roles=['nm-controller', 'self'], 
180                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
181                                    returns=Parameter(dict, "A resource specification"))
182 @export_to_api(1)
183 def GetEffectiveRSpec(sliver_name):
184     """Return the RSpec allocated to the specified sliver, including loans"""
185     rec = sliver_name
186     return rec.get('_rspec', {}).copy()
187
188 @export_to_docbook(roles=['nm-controller', 'self'], 
189                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
190                                     returns={
191                                                         "resource name" : Parameter(int, "amount")
192                                                 }
193                                   )
194 @export_to_api(1)
195 def GetRSpec(sliver_name):
196     """Return the RSpec allocated to the specified sliver, excluding loans"""
197     rec = sliver_name
198     return rec.get('rspec', {}).copy()
199
200 @export_to_docbook(roles=['nm-controller', 'self'], 
201                                         accepts=[Parameter(str, 'A sliver/slice name.')], 
202                                         returns=[Mixed(Parameter(str, 'recipient slice name'),
203                                                      Parameter(str, 'resource name'),
204                                                      Parameter(int, 'resource amount'))] 
205                                   )
206 @export_to_api(1)
207 def GetLoans(sliver_name):
208     """Return the list of loans made by the specified sliver"""
209     rec = sliver_name
210     return rec.get('_loans', [])[:]
211
212 def validate_loans(obj):
213     """Check that <obj> is a valid loan specification."""
214     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
215     return type(obj)==list and False not in map(validate_loan, obj)
216
217 @export_to_docbook(roles=['nm-controller', 'self'], 
218                                 accepts=[ Parameter(str, 'A sliver/slice name.'),
219                                                   [Mixed(Parameter(str, 'recipient slice name'),
220                                                    Parameter(str, 'resource name'),
221                                                    Parameter(int, 'resource amount'))] ],
222                                 returns=Parameter(int, '1 if successful'))
223 @export_to_api(2)
224 def SetLoans(sliver_name, loans):
225     """Overwrite the list of loans made by the specified sliver.
226
227         Also, note that SetLoans will not throw an error if more capacity than the
228         RSpec is handed out, but it will silently discard those loans that would
229         put it over capacity.  This behavior may be replaced with error semantics
230         in the future.  As well, there is currently no asynchronous notification
231         of loss of resources.
232         """
233     rec = sliver_name
234     if not validate_loans(loans): raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
235     rec['_loans'] = loans
236     database.db.sync()