Making node.py code independent from the plcapi implementation.
[nepi.git] / src / nepi / testbeds / planetlab / plcapi.py
1 import xmlrpclib
2 import functools
3 import socket
4 import time
5 import threading
6
7 def _retry(fn):
8     def rv(*p, **kw):
9         for x in xrange(5):
10             try:
11                 return fn(*p, **kw)
12             except (socket.error, IOError, OSError):
13                 time.sleep(x*5+5)
14         else:
15             return fn (*p, **kw)
16     return rv
17
18 class PLCAPI(object):
19
20     _expected_methods = set(
21         ['AddNodeTag', 'AddConfFile', 'DeletePersonTag', 'AddNodeType', 'DeleteBootState', 'SliceListNames', 'DeleteKey', 
22          'SliceGetTicket', 'SliceUsersList', 'SliceUpdate', 'GetNodeGroups', 'SliceCreate', 'GetNetworkMethods', 'GetNodeFlavour', 
23          'DeleteNode', 'BootNotifyOwners', 'AddPersonKey', 'AddNode', 'UpdateNodeGroup', 'GetAddressTypes', 'AddIlink', 'DeleteNetworkType', 
24          'GetInitScripts', 'GenerateNodeConfFile', 'AddSite', 'BindObjectToPeer', 'SliceListUserSlices', 'GetPeers', 'AddPeer', 'DeletePeer', 
25          'AddRole', 'DeleteRole', 'SetPersonPrimarySite', 'AddSiteAddress', 'SliceDelete', 'NotifyPersons', 'GetKeyTypes', 'GetConfFiles', 
26          'GetIlinks', 'AddTagType', 'GetNodes', 'DeleteNodeTag', 'DeleteSliceFromNodesWhitelist', 'UpdateAddress', 'ResetPassword', 
27          'AddSliceToNodesWhitelist', 'AddRoleToTagType', 'AddLeases', 'GetAddresses', 'AddInitScript', 'RebootNode', 'GetPCUTypes', 
28          'RefreshPeer', 'GetBootMedium', 'UpdateKey', 'UpdatePCU', 'GetSession', 'AddInterfaceTag', 'UpdatePCUType', 'GetInterfaces', 
29          'SliceExtendedInfo', 'SliceNodesList', 'DeleteRoleFromTagType', 'DeleteSlice', 'GetSites', 'DeleteMessage', 'GetSliceFamily', 
30          'GetPlcRelease', 'UpdateTagType', 'AddSliceInstantiation', 'ResolveSlices', 'GetSlices', 'DeleteRoleFromPerson', 'GetSessions', 
31          'UpdatePeer', 'VerifyPerson', 'GetPersonTags', 'DeleteKeyType', 'AddSlice', 'SliceUserAdd', 'DeleteSession', 'GetMessages', 
32          'DeletePCU', 'GetPeerData', 'DeletePersonFromSite', 'DeleteTagType', 'GetPCUs', 'UpdateLeases', 'AddMessage', 
33          'DeletePCUProtocolType', 'DeleteInterfaceTag', 'AddPersonToSite', 'GetSlivers', 'SliceNodesDel', 'DeleteAddressTypeFromAddress', 
34          'AddNodeGroup', 'GetSliceTags', 'DeleteSite', 'GetSiteTags', 'UpdateMessage', 'DeleteSliceFromNodes', 'SliceRenew', 
35          'UpdatePCUProtocolType', 'DeleteSiteTag', 'GetPCUProtocolTypes', 'GetEvents', 'GetSliceTicket', 'AddPersonTag', 'BootGetNodeDetails', 
36          'DeleteInterface', 'DeleteNodeGroup', 'AddPCUProtocolType', 'BootCheckAuthentication', 'AddSiteTag', 'AddAddressTypeToAddress', 
37          'DeleteConfFile', 'DeleteInitScript', 'DeletePerson', 'DeleteIlink', 'DeleteAddressType', 'AddBootState', 'AuthCheck', 
38          'NotifySupport', 'GetSliceInstantiations', 'AddPCUType', 'AddPCU', 'AddSession', 'GetEventObjects', 'UpdateSiteTag', 
39          'UpdateNodeTag', 'AddPerson', 'BlacklistKey', 'UpdateInitScript', 'AddSliceToNodes', 'RebootNodeWithPCU', 'GetNodeTags', 
40          'GetSliceKeys', 'GetSliceSshKeys', 'AddNetworkMethod', 'SliceNodesAdd', 'DeletePersonFromSlice', 'ReportRunlevel', 
41          'GetNetworkTypes', 'UpdateSite', 'DeleteConfFileFromNodeGroup', 'UpdateNode', 'DeleteSliceInstantiation', 'DeleteSliceTag', 
42          'BootUpdateNode', 'UpdatePerson', 'UpdateConfFile', 'SliceUserDel', 'DeleteLeases', 'AddConfFileToNodeGroup', 'UpdatePersonTag', 
43          'DeleteConfFileFromNode', 'AddPersonToSlice', 'UnBindObjectFromPeer', 'AddNodeToPCU', 'GetLeaseGranularity', 'DeletePCUType', 
44          'GetTagTypes', 'GetNodeTypes', 'UpdateInterfaceTag', 'GetRoles', 'UpdateSlice', 'UpdateSliceTag', 'AddSliceTag', 'AddNetworkType', 
45          'AddInterface', 'AddAddressType', 'AddRoleToPerson', 'DeleteNodeType', 'GetLeases', 'UpdateInterface', 'SliceInfo', 'DeleteAddress', 
46          'SliceTicketGet', 'GetPersons', 'GetWhitelist', 'AddKeyType', 'UpdateAddressType', 'GetPeerName', 'DeleteNetworkMethod', 
47          'UpdateIlink', 'AddConfFileToNode', 'GetKeys', 'DeleteNodeFromPCU', 'GetInterfaceTags', 'GetBootStates', 'SetInterfaceSens', 'SetNodeLoadm', 
48          'GetInterfaceRate', 'GetNodeLoadw', 'SetInterfaceKey', 'GetNodeSlices', 'GetNodeLoadm', 'SetSliceVref', 'GetInterfaceIwpriv', 'SetNodeLoadw', 
49          'SetNodeSerial', 'GetNodePlainBootstrapfs', 'SetNodeMEMw', 'GetNodeResponse', 'SetInterfaceRate', 'SetSliceInitscript', 
50          'SetNodeFcdistro', 'GetNodeLoady', 'SetNodeArch', 'SetNodeKargs', 'SetNodeMEMm', 'SetNodeBWy', 'SetNodeBWw', 
51          'SetInterfaceSecurityMode', 'SetNodeBWm', 'SetNodeASType', 'GetNodeKargs', 'GetPersonColumnconf', 'GetNodeResponsem', 
52          'GetNodeCPUy', 'GetNodeCramfs', 'SetNodeSlicesw', 'SetPersonColumnconf', 'SetNodeSlicesy', 'GetNodeCPUw', 'GetNodeBWy', 
53          'GetNodeCPUm', 'GetInterfaceDriver', 'GetNodeLoad', 'GetInterfaceMode', 'GetNodeSerial', 'SetNodeSlicesm', 'SetNodeLoady', 
54          'GetNodeReliabilityw', 'SetSliceFcdistro', 'GetNodeReliabilityy', 'SetInterfaceEssid', 'SetSliceInitscriptCode', 
55          'GetNodeExtensions', 'GetSliceOmfControl', 'SetNodeCity', 'SetInterfaceIfname', 'SetNodeHrn', 'SetNodeNoHangcheck', 
56          'GetNodeNoHangcheck', 'GetSliceFcdistro', 'SetNodeCountry', 'SetNodeKvariant', 'GetNodeKvariant', 'GetNodeMEMy', 
57          'SetInterfaceIwpriv', 'GetNodeMEMw', 'SetInterfaceBackdoor', 'GetInterfaceFreq', 'SetInterfaceChannel', 'SetInterfaceNw', 
58          'GetPersonShowconf', 'GetSliceInitscriptCode', 'SetNodeMEM', 'GetInterfaceEssid', 'GetNodeMEMm', 'SetInterfaceMode', 
59          'SetInterfaceIwconfig', 'GetNodeSlicesm', 'GetNodeBWm', 'SetNodePlainBootstrapfs', 'SetNodeRegion', 'SetNodeCPU', 
60          'GetNodeSlicesw', 'SetNodeBW', 'SetNodeSlices', 'SetNodeCramfs', 'GetNodeSlicesy', 'GetInterfaceKey', 'GetSliceInitscript', 
61          'SetNodeCPUm', 'SetSliceArch', 'SetNodeLoad', 'SetNodeResponse', 'GetSliceSliverHMAC', 'GetNodeBWw', 'GetNodeRegion', 
62          'SetNodeMEMy', 'GetNodeASType', 'SetNodePldistro', 'GetSliceArch', 'GetNodeCountry', 'SetSliceOmfControl', 'GetNodeHrn', 
63          'GetNodeCity', 'SetInterfaceAlias', 'GetNodeBW', 'GetNodePldistro', 'GetSlicePldistro', 'SetNodeASNumber', 'GetSliceHmac', 
64          'SetSliceHmac', 'GetNodeMEM', 'GetNodeASNumber', 'GetInterfaceAlias', 'GetSliceVref', 'GetNodeArch', 'GetSliceSshKey', 
65          'GetInterfaceKey4', 'GetInterfaceKey2', 'GetInterfaceKey3', 'GetInterfaceKey1', 'GetInterfaceBackdoor', 'GetInterfaceIfname', 
66          'SetSliceSliverHMAC', 'SetNodeReliability', 'GetNodeCPU', 'SetPersonShowconf', 'SetNodeExtensions', 'SetNodeCPUy', 
67          'SetNodeCPUw', 'GetNodeResponsew', 'SetNodeResponsey', 'GetInterfaceSens', 'SetNodeResponsew', 'GetNodeResponsey', 
68          'GetNodeReliability', 'GetNodeReliabilitym', 'SetNodeResponsem', 'SetInterfaceDriver', 'GetInterfaceSecurityMode', 
69          'SetNodeDeployment', 'SetNodeReliabilitym', 'GetNodeFcdistro', 'SetInterfaceFreq', 'GetInterfaceNw', 'SetNodeReliabilityy', 
70          'SetNodeReliabilityw', 'GetInterfaceIwconfig', 'SetSlicePldistro', 'SetSliceSshKey', 'GetNodeDeployment', 'GetInterfaceChannel', 
71          'SetInterfaceKey2', 'SetInterfaceKey3', 'SetInterfaceKey1', 'SetInterfaceKey4'])
72      
73     _required_methods = set()
74
75     def __init__(self, username=None, password=None, sessionkey=None,
76             hostname = "www.planet-lab.eu",
77             urlpattern = "https://%(hostname)s:443/PLCAPI/",
78             localPeerName = "PLE"):
79         if sessionkey is not None:
80             self.auth = dict(AuthMethod='session', session=sessionkey)
81         elif username is not None and password is not None:
82             self.auth = dict(AuthMethod='password', Username=username, AuthString=password)
83         else:
84             self.auth = dict(AuthMethod='anonymous')
85         
86         self._localPeerName = localPeerName
87         self._url = urlpattern % {'hostname':hostname}
88         
89         self.threadlocal = threading.local()
90     
91     @property
92     def api(self):
93         # Cannot reuse same proxy in all threads, py2.7 is not threadsafe
94         return xmlrpclib.ServerProxy(
95             self._url ,
96             allow_none = True)
97         
98     @property
99     def mcapi(self):
100         try:
101             return self.threadlocal.mc
102         except AttributeError:
103             return self.api
104         
105     def test(self):
106         import warnings
107         
108         # validate XMLRPC server checking supported API calls
109         methods = set(_retry(self.mcapi.system.listMethods)())
110         if self._required_methods - methods:
111             warnings.warn("Unsupported REQUIRED methods: %s" % ( ", ".join(sorted(self._required_methods - methods)), ) )
112             return False
113         if self._expected_methods - methods:
114             warnings.warn("Unsupported EXPECTED methods: %s" % ( ", ".join(sorted(self._expected_methods - methods)), ) )
115         
116         try:
117             # test authorization
118             network_types = _retry(self.mcapi.GetNetworkTypes)(self.auth)
119         except (xmlrpclib.ProtocolError, xmlrpclib.Fault),e:
120             warnings.warn(str(e))
121         
122         return True
123     
124     
125     @property
126     def network_types(self):
127         try:
128             return self._network_types
129         except AttributeError:
130             self._network_types = _retry(self.mcapi.GetNetworkTypes)(self.auth)
131             return self._network_types
132     
133     @property
134     def peer_map(self):
135         try:
136             return self._peer_map
137         except AttributeError:
138             peers = _retry(self.mcapi.GetPeers)(self.auth, {}, ['shortname','peername','peer_id'])
139             self._peer_map = dict(
140                 (peer['shortname'], peer['peer_id'])
141                 for peer in peers
142             )
143             self._peer_map.update(
144                 (peer['peername'], peer['peer_id'])
145                 for peer in peers
146             )
147             self._peer_map.update(
148                 (peer['peer_id'], peer['shortname'])
149                 for peer in peers
150             )
151             self._peer_map[None] = self._localPeerName
152             return self._peer_map
153     
154
155     def GetNodeFlavour(self, node):
156         """
157         Returns detailed information on a given node's flavour, i.e. its base installation.
158
159         This depends on the global PLC settings in the PLC_FLAVOUR area, optionnally overridden by any of the following tags if set on that node:
160         'arch', 'pldistro', 'fcdistro', 'deployment', 'extensions'
161         
162         Params:
163         
164             * node : int or string
165                 - int, Node identifier
166                 - string, Fully qualified hostname
167         
168         Returns:
169
170             struct
171                 * extensions : array of string, extensions to add to the base install
172                 * fcdistro : string, the fcdistro this node should be based upon
173                 * nodefamily : string, the nodefamily this node should be based upon
174                 * plain : boolean, use plain bootstrapfs image if set (for tests)  
175         """
176         if not isinstance(node, (str, int, long)):
177             raise ValueError, "Node must be either a non-unicode string or an int"
178         return _retry(self.mcapi.GetNodeFlavour)(self.auth, node)
179     
180     def GetNodes(self, nodeIdOrName=None, fields=None, **kw):
181         """
182         Returns an array of structs containing details about nodes. 
183         If nodeIdOrName is specified and is an array of node identifiers or hostnames, 
184         or the filters keyword argument with struct of node attributes, 
185         or node attributes by keyword argument,
186         only nodes matching the filter will be returned.
187
188         If fields is specified, only the specified details will be returned. 
189         NOTE that if fields is unspecified, the complete set of native fields are returned, 
190         which DOES NOT include tags at this time.
191
192         Some fields may only be viewed by admins.
193         
194         Special params:
195             
196             fields: an optional list of fields to retrieve. The default is all.
197             
198             filters: an optional mapping with custom filters, which is the only
199                 way to support complex filters like negation and numeric comparisons.
200                 
201             peer: a string (or sequence of strings) with the name(s) of peers
202                 to filter - or None for local nodes.
203         """
204         if fields is not None:
205             fieldstuple = (fields,)
206         else:
207             fieldstuple = ()
208         if nodeIdOrName is not None:
209             return _retry(self.mcapi.GetNodes)(self.auth, nodeIdOrName, *fieldstuple)
210         else:
211             filters = kw.pop('filters',{})
212             
213             if 'peer' in kw:
214                 peer = kw.pop('peer')
215                 
216                 nameToId = self.peer_map.get
217                 
218                 if hasattr(peer, '__iter__'):
219                     # we can't mix local and external nodes, so
220                     # split and re-issue recursively in that case
221                     if None in peer or self._localPeerName in peer:
222                         if None in peer:    
223                             peer.remove(None)
224                         if self._localPeerName in peer:
225                             peer.remove(self._localPeerName)
226                         return (
227                             self.GetNodes(nodeIdOrName, fields, filters=filters, peer=peer, **kw)
228                             + self.GetNodes(nodeIdOrName, fields, filters=filters, peer=None, **kw)
229                         )
230                     else:
231                         peer_filter = map(nameToId, peer)
232                 elif peer is None or peer == self._localPeerName:
233                     peer_filter = None
234                 else:
235                     peer_filter = nameToId(peer)
236                 
237                 filters['peer_id'] = peer_filter
238             
239             filters.update(kw)
240             return _retry(self.mcapi.GetNodes)(self.auth, filters, *fieldstuple)
241     
242     def GetNodeTags(self, nodeTagId=None, fields=None, **kw):
243         if fields is not None:
244             fieldstuple = (fields,)
245         else:
246             fieldstuple = ()
247         if nodeTagId is not None:
248             return _retry(self.mcapi.GetNodeTags)(self.auth, nodeTagId, *fieldstuple)
249         else:
250             filters = kw.pop('filters',{})
251             filters.update(kw)
252             return _retry(self.mcapi.GetNodeTags)(self.auth, filters, *fieldstuple)
253
254     def GetSliceTags(self, sliceTagId=None, fields=None, **kw):
255         if fields is not None:
256             fieldstuple = (fields,)
257         else:
258             fieldstuple = ()
259         if sliceTagId is not None:
260             return _retry(self.mcapi.GetSliceTags)(self.auth, sliceTagId, *fieldstuple)
261         else:
262             filters = kw.pop('filters',{})
263             filters.update(kw)
264             return _retry(self.mcapi.GetSliceTags)(self.auth, filters, *fieldstuple)
265         
266     
267     def GetInterfaces(self, interfaceIdOrIp=None, fields=None, **kw):
268         if fields is not None:
269             fieldstuple = (fields,)
270         else:
271             fieldstuple = ()
272         if interfaceIdOrIp is not None:
273             return _retry(self.mcapi.GetInterfaces)(self.auth, interfaceIdOrIp, *fieldstuple)
274         else:
275             filters = kw.pop('filters',{})
276             filters.update(kw)
277             return _retry(self.mcapi.GetInterfaces)(self.auth, filters, *fieldstuple)
278         
279     def GetSlices(self, sliceIdOrName=None, fields=None, **kw):
280         if fields is not None:
281             fieldstuple = (fields,)
282         else:
283             fieldstuple = ()
284         if sliceIdOrName is not None:
285             return _retry(self.mcapi.GetSlices)(self.auth, sliceIdOrName, *fieldstuple)
286         else:
287             filters = kw.pop('filters',{})
288             filters.update(kw)
289             return _retry(self.mcapi.GetSlices)(self.auth, filters, *fieldstuple)
290         
291     def UpdateSlice(self, sliceIdOrName, **kw):
292         return _retry(self.mcapi.UpdateSlice)(self.auth, sliceIdOrName, kw)
293         
294
295     def StartMulticall(self):
296         self.threadlocal.mc = xmlrpclib.MultiCall(self.mcapi)
297     
298     def FinishMulticall(self):
299         mc = self.threadlocal.mc
300         del self.threadlocal.mc
301         return _retry(mc)()
302
303     def GetSliceNodes(self, slicename):
304         return self.GetSlices(slicename, ['node_ids'])[0]['node_ids']
305
306     def AddSliceNodes(self, slicename,  nodes = None):
307         self.UpdateSlice(slicename, nodes = nodes)
308
309     def GetNodeInfo(self, node_id):
310         self.StartMulticall()
311         info = self.GetNodes(node_id)
312         tags = self.GetNodeTags(node_id=node_id, fields=('tagname','value'))
313         info, tags = self.FinishMulticall()
314         return info, tags
315
316     def GetSliceId(self, slicename):
317         slice_id = None
318         slices = self.GetSlices(slicename, fields=('slice_id',))
319         if slices:
320                 slice_id = slices[0]['slice_id']
321         return slice_id
322  
323 def plcapi(auth_user, auth_string, plc_host, plc_url):
324     api = None
325     if auth_user:
326         api = PLCAPI(
327             username = auth_user,
328             password = auth_string,
329             hostname = plc_host,
330             urlpattern = plc_url
331         )
332     else:
333         # anonymous access - may not be enough for much
334         api = PLCAPI()
335     return api
336
337