Merging patch to support https proxies when running experiments using PlanetLab,...
[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, proxy=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         if (proxy is not None):
89             import urllib2
90             class HTTPSProxyTransport(xmlrpclib.Transport):
91                 def __init__(self, proxy, use_datetime=0):
92                     opener = urllib2.build_opener(urllib2.ProxyHandler({"https" : proxy}))
93                     xmlrpclib.Transport.__init__(self, use_datetime)
94                     self.opener = opener
95                 def request(self, host, handler, request_body, verbose=0):
96                     req = urllib2.Request('https://%s%s' % (host, handler), request_body)
97                     req.add_header('User-agent', self.user_agent)
98                     self.verbose = verbose
99                     return self.parse_response(self.opener.open(req))
100             self._proxyTransport = lambda : HTTPSProxyTransport(proxy)
101         else:
102             self._proxyTransport = lambda : None
103         
104         self.threadlocal = threading.local()
105     
106     @property
107     def api(self):
108         # Cannot reuse same proxy in all threads, py2.7 is not threadsafe
109         return xmlrpclib.ServerProxy(
110             self._url ,
111             transport = self._proxyTransport(),
112             allow_none = True)
113         
114     @property
115     def mcapi(self):
116         try:
117             return self.threadlocal.mc
118         except AttributeError:
119             return self.api
120         
121     def test(self):
122         import warnings
123         
124         # validate XMLRPC server checking supported API calls
125         methods = set(_retry(self.mcapi.system.listMethods)())
126         if self._required_methods - methods:
127             warnings.warn("Unsupported REQUIRED methods: %s" % ( ", ".join(sorted(self._required_methods - methods)), ) )
128             return False
129         if self._expected_methods - methods:
130             warnings.warn("Unsupported EXPECTED methods: %s" % ( ", ".join(sorted(self._expected_methods - methods)), ) )
131         
132         try:
133             # test authorization
134             network_types = _retry(self.mcapi.GetNetworkTypes)(self.auth)
135         except (xmlrpclib.ProtocolError, xmlrpclib.Fault),e:
136             warnings.warn(str(e))
137         
138         return True
139     
140     
141     @property
142     def network_types(self):
143         try:
144             return self._network_types
145         except AttributeError:
146             self._network_types = _retry(self.mcapi.GetNetworkTypes)(self.auth)
147             return self._network_types
148     
149     @property
150     def peer_map(self):
151         try:
152             return self._peer_map
153         except AttributeError:
154             peers = _retry(self.mcapi.GetPeers)(self.auth, {}, ['shortname','peername','peer_id'])
155             self._peer_map = dict(
156                 (peer['shortname'], peer['peer_id'])
157                 for peer in peers
158             )
159             self._peer_map.update(
160                 (peer['peername'], peer['peer_id'])
161                 for peer in peers
162             )
163             self._peer_map.update(
164                 (peer['peer_id'], peer['shortname'])
165                 for peer in peers
166             )
167             self._peer_map[None] = self._localPeerName
168             return self._peer_map
169     
170
171     def GetNodeFlavour(self, node):
172         """
173         Returns detailed information on a given node's flavour, i.e. its base installation.
174
175         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:
176         'arch', 'pldistro', 'fcdistro', 'deployment', 'extensions'
177         
178         Params:
179         
180             * node : int or string
181                 - int, Node identifier
182                 - string, Fully qualified hostname
183         
184         Returns:
185
186             struct
187                 * extensions : array of string, extensions to add to the base install
188                 * fcdistro : string, the fcdistro this node should be based upon
189                 * nodefamily : string, the nodefamily this node should be based upon
190                 * plain : boolean, use plain bootstrapfs image if set (for tests)  
191         """
192         if not isinstance(node, (str, int, long)):
193             raise ValueError, "Node must be either a non-unicode string or an int"
194         return _retry(self.mcapi.GetNodeFlavour)(self.auth, node)
195     
196     def GetNodes(self, nodeIdOrName=None, fields=None, **kw):
197         """
198         Returns an array of structs containing details about nodes. 
199         If nodeIdOrName is specified and is an array of node identifiers or hostnames, 
200         or the filters keyword argument with struct of node attributes, 
201         or node attributes by keyword argument,
202         only nodes matching the filter will be returned.
203
204         If fields is specified, only the specified details will be returned. 
205         NOTE that if fields is unspecified, the complete set of native fields are returned, 
206         which DOES NOT include tags at this time.
207
208         Some fields may only be viewed by admins.
209         
210         Special params:
211             
212             fields: an optional list of fields to retrieve. The default is all.
213             
214             filters: an optional mapping with custom filters, which is the only
215                 way to support complex filters like negation and numeric comparisons.
216                 
217             peer: a string (or sequence of strings) with the name(s) of peers
218                 to filter - or None for local nodes.
219         """
220         if fields is not None:
221             fieldstuple = (fields,)
222         else:
223             fieldstuple = ()
224         if nodeIdOrName is not None:
225             return _retry(self.mcapi.GetNodes)(self.auth, nodeIdOrName, *fieldstuple)
226         else:
227             filters = kw.pop('filters',{})
228             
229             if 'peer' in kw:
230                 peer = kw.pop('peer')
231                 
232                 nameToId = self.peer_map.get
233                 
234                 if hasattr(peer, '__iter__'):
235                     # we can't mix local and external nodes, so
236                     # split and re-issue recursively in that case
237                     if None in peer or self._localPeerName in peer:
238                         if None in peer:    
239                             peer.remove(None)
240                         if self._localPeerName in peer:
241                             peer.remove(self._localPeerName)
242                         return (
243                             self.GetNodes(nodeIdOrName, fields, filters=filters, peer=peer, **kw)
244                             + self.GetNodes(nodeIdOrName, fields, filters=filters, peer=None, **kw)
245                         )
246                     else:
247                         peer_filter = map(nameToId, peer)
248                 elif peer is None or peer == self._localPeerName:
249                     peer_filter = None
250                 else:
251                     peer_filter = nameToId(peer)
252                 
253                 filters['peer_id'] = peer_filter
254             
255             filters.update(kw)
256             return _retry(self.mcapi.GetNodes)(self.auth, filters, *fieldstuple)
257     
258     def GetNodeTags(self, nodeTagId=None, fields=None, **kw):
259         if fields is not None:
260             fieldstuple = (fields,)
261         else:
262             fieldstuple = ()
263         if nodeTagId is not None:
264             return _retry(self.mcapi.GetNodeTags)(self.auth, nodeTagId, *fieldstuple)
265         else:
266             filters = kw.pop('filters',{})
267             filters.update(kw)
268             return _retry(self.mcapi.GetNodeTags)(self.auth, filters, *fieldstuple)
269
270     def GetSliceTags(self, sliceTagId=None, fields=None, **kw):
271         if fields is not None:
272             fieldstuple = (fields,)
273         else:
274             fieldstuple = ()
275         if sliceTagId is not None:
276             return _retry(self.mcapi.GetSliceTags)(self.auth, sliceTagId, *fieldstuple)
277         else:
278             filters = kw.pop('filters',{})
279             filters.update(kw)
280             return _retry(self.mcapi.GetSliceTags)(self.auth, filters, *fieldstuple)
281     
282     def GetInterfaces(self, interfaceIdOrIp=None, fields=None, **kw):
283         if fields is not None:
284             fieldstuple = (fields,)
285         else:
286             fieldstuple = ()
287         if interfaceIdOrIp is not None:
288             return _retry(self.mcapi.GetInterfaces)(self.auth, interfaceIdOrIp, *fieldstuple)
289         else:
290             filters = kw.pop('filters',{})
291             filters.update(kw)
292             return _retry(self.mcapi.GetInterfaces)(self.auth, filters, *fieldstuple)
293         
294     def GetSlices(self, sliceIdOrName=None, fields=None, **kw):
295         if fields is not None:
296             fieldstuple = (fields,)
297         else:
298             fieldstuple = ()
299         if sliceIdOrName is not None:
300             return _retry(self.mcapi.GetSlices)(self.auth, sliceIdOrName, *fieldstuple)
301         else:
302             filters = kw.pop('filters',{})
303             filters.update(kw)
304             return _retry(self.mcapi.GetSlices)(self.auth, filters, *fieldstuple)
305         
306     def UpdateSlice(self, sliceIdOrName, **kw):
307         return _retry(self.mcapi.UpdateSlice)(self.auth, sliceIdOrName, kw)
308
309     def StartMulticall(self):
310         self.threadlocal.mc = xmlrpclib.MultiCall(self.mcapi)
311     
312     def FinishMulticall(self):
313         mc = self.threadlocal.mc
314         del self.threadlocal.mc
315         return _retry(mc)()
316
317     def GetSliceNodes(self, slicename):
318         return self.GetSlices(slicename, ['node_ids'])[0]['node_ids']
319
320     def AddSliceNodes(self, slicename,  nodes = None):
321         self.UpdateSlice(slicename, nodes = nodes)
322
323     def GetNodeInfo(self, node_id):
324         self.StartMulticall()
325         info = self.GetNodes(node_id)
326         tags = self.GetNodeTags(node_id=node_id, fields=('tagname','value'))
327         info, tags = self.FinishMulticall()
328         return info, tags
329
330     def GetSliceId(self, slicename):
331         slice_id = None
332         slices = self.GetSlices(slicename, fields=('slice_id',))
333         if slices:
334             slice_id = slices[0]['slice_id']
335         # If it wasn't found, don't remember this failure, keep trying
336         return slice_id
337
338     def GetSliceVnetSysTag(self, slicename):
339         slicetags = self.GetSliceTags(
340             name = slicename,
341             tagname = 'vsys_vnet',
342             fields=('value',))
343         if slicetags:
344             return slicetags[0]['value']
345         else:
346             return None
347  
348 def plcapi(auth_user, auth_string, plc_host, plc_url, proxy):
349     api = None
350     if auth_user:
351         api = PLCAPI(
352             username = auth_user,
353             password = auth_string,
354             hostname = plc_host,
355             urlpattern = plc_url,
356             proxy = proxy
357         )
358     else:
359         # anonymous access - may not be enough for much
360         api = PLCAPI()
361     return api
362
363