* Explicitly run configure, which sets capabilities and resources, before starting...
[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=[], 
126                    returns={'sliver_name' : Parameter(int, 'the associated xid')})
127 @export_to_api(0)
128 def GetXIDs():
129     """Return an dictionary mapping Slice names to XIDs"""
130     return dict([(pwent[0], pwent[2]) for pwent in pwd.getpwall() if pwent[6] == sliver_vs.Sliver_VS.SHELL])
131
132 @export_to_docbook(roles=['self'],
133                    accepts=[], 
134                    returns={ 'sliver_name' : Parameter(str, 'the associated SSHKey')})
135 @export_to_api(0)
136 def GetSSHKeys():
137     """Return an dictionary mapping slice names to SSH keys"""
138     keydict = {}
139     for rec in database.db.itervalues():
140         if 'keys' in rec:
141             keydict[rec['name']] = rec['keys']
142     return keydict
143
144 @export_to_docbook(roles=['nm-controller', 'self'], 
145                     accepts=[Parameter(str, 'A sliver/slice name.')], 
146                    returns=Parameter(int, '1 if successful'))
147 @export_to_api(1)
148 def Create(sliver_name):
149     """Create a non-PLC-instantiated sliver"""
150     rec = sliver_name
151     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_created(rec)
152
153 @export_to_docbook(roles=['nm-controller', 'self'], 
154                     accepts=[Parameter(str, 'A sliver/slice name.')], 
155                    returns=Parameter(int, '1 if successful'))
156 @export_to_api(1)
157 def Destroy(sliver_name):
158     """Destroy a non-PLC-instantiated sliver"""
159     rec = sliver_name 
160     if rec['instantiation'] == 'delegated': accounts.get(rec['name']).ensure_destroyed()
161
162 @export_to_docbook(roles=['nm-controller', 'self'], 
163                     accepts=[Parameter(str, 'A sliver/slice name.')], 
164                    returns=Parameter(int, '1 if successful'))
165 @export_to_api(1)
166 def Start(sliver_name):
167     """Configure and start sliver."""
168     rec = sliver_name
169     accounts.get(rec['name']).start(rec)
170
171 @export_to_docbook(roles=['nm-controller', 'self'], 
172                     accepts=[Parameter(str, 'A sliver/slice name.')], 
173                    returns=Parameter(int, '1 if successful'))
174 @export_to_api(1)
175 def Stop(sliver_name):
176     """Kill all processes belonging to the specified sliver"""
177     rec = sliver_name
178     accounts.get(rec['name']).stop()
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
184 @export_to_api(1)
185 def ReCreate(sliver_name):
186     """Stop, Destroy, Create, Start sliver in order to reinstall it."""
187     Stop(sliver_name)
188     Destroy(sliver_name)
189     Create(sliver_name)
190
191 @export_to_docbook(roles=['nm-controller', 'self'], 
192                     accepts=[Parameter(str, 'A sliver/slice name.')], 
193                    returns=Parameter(dict, "A resource specification"))
194 @export_to_api(1)
195 def GetEffectiveRSpec(sliver_name):
196     """Return the RSpec allocated to the specified sliver, including 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={
203                             "resource name" : Parameter(int, "amount")
204                         }
205                   )
206 @export_to_api(1)
207 def GetRSpec(sliver_name):
208     """Return the RSpec allocated to the specified sliver, excluding loans"""
209     rec = sliver_name
210     return rec.get('rspec', {}).copy()
211
212 @export_to_docbook(roles=['nm-controller', 'self'], 
213                     accepts=[Parameter(str, 'A sliver/slice name.')], 
214                     returns=[Mixed(Parameter(str, 'recipient slice name'),
215                              Parameter(str, 'resource name'),
216                              Parameter(int, 'resource amount'))] 
217                   )
218 @export_to_api(1)
219 def GetLoans(sliver_name):
220     """Return the list of loans made by the specified sliver"""
221     rec = sliver_name
222     return rec.get('_loans', [])[:]
223
224 def validate_loans(obj):
225     """Check that <obj> is a valid loan specification."""
226     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
227     return type(obj)==list and False not in map(validate_loan, obj)
228
229 @export_to_docbook(roles=['nm-controller', 'self'], 
230                 accepts=[ Parameter(str, 'A sliver/slice name.'),
231                           [Mixed(Parameter(str, 'recipient slice name'),
232                            Parameter(str, 'resource name'),
233                            Parameter(int, 'resource amount'))] ],
234                 returns=Parameter(int, '1 if successful'))
235 @export_to_api(2)
236 def SetLoans(sliver_name, loans):
237     """Overwrite the list of loans made by the specified sliver.
238
239     Also, note that SetLoans will not throw an error if more capacity than the
240     RSpec is handed out, but it will silently discard those loans that would
241     put it over capacity.  This behavior may be replaced with error semantics
242     in the future.  As well, there is currently no asynchronous notification
243     of loss of resources.
244     """
245     rec = sliver_name
246     if not validate_loans(loans): raise xmlrpclib.Fault(102, 'Invalid argument: the second argument must be a well-formed loan specification')
247     rec['_loans'] = loans
248     database.db.sync()