Adding authors and correcting licence information
[nepi.git] / src / nepi / resources / planetlab / plcapi.py
1 #
2 #    NEPI, a framework to manage network experiments
3 #    Copyright (C) 2013 INRIA
4 #
5 #    This program is free software: you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation, either version 3 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14 #
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18 # Author: Alina Quereilhac <alina.quereilhac@inria.fr>
19
20 import functools
21 import hashlib
22 import socket
23 import time
24 import threading
25 import xmlrpclib
26
27 def _retry(fn):
28     def rv(*p, **kw):
29         for x in xrange(5):
30             try:
31                 return fn(*p, **kw)
32             except (socket.error, IOError, OSError):
33                 time.sleep(x*5+5)
34         else:
35             return fn (*p, **kw)
36     return rv
37
38 class PLCAPI(object):
39
40     _expected_methods = set(
41         ['AddNodeTag', 'AddConfFile', 'DeletePersonTag', 'AddNodeType',
42          'DeleteBootState', 'SliceListNames', 'DeleteKey','SliceGetTicket', 
43          'SliceUsersList', 'SliceUpdate', 'GetNodeGroups', 'SliceCreate', 
44          'GetNetworkMethods', 'GetNodeFlavour', 'DeleteNode', 'BootNotifyOwners',
45          'AddPersonKey', 'AddNode', 'UpdateNodeGroup', 'GetAddressTypes',
46          'AddIlink', 'DeleteNetworkType', 'GetInitScripts', 'GenerateNodeConfFile',
47          'AddSite', 'BindObjectToPeer', 'SliceListUserSlices', 'GetPeers',
48          'AddPeer', 'DeletePeer', 'AddRole', 'DeleteRole', 'SetPersonPrimarySite',
49          'AddSiteAddress', 'SliceDelete', 'NotifyPersons', 'GetKeyTypes',
50          'GetConfFiles', 'GetIlinks', 'AddTagType', 'GetNodes', 'DeleteNodeTag',
51          'DeleteSliceFromNodesWhitelist', 'UpdateAddress', 'ResetPassword', 
52          'AddSliceToNodesWhitelist', 'AddRoleToTagType', 'AddLeases',
53          'GetAddresses', 'AddInitScript', 'RebootNode', 'GetPCUTypes', 
54          'RefreshPeer', 'GetBootMedium', 'UpdateKey', 'UpdatePCU', 'GetSession',
55          'AddInterfaceTag', 'UpdatePCUType', 'GetInterfaces', 'SliceExtendedInfo',
56          'SliceNodesList', 'DeleteRoleFromTagType', 'DeleteSlice', 'GetSites',
57          'DeleteMessage', 'GetSliceFamily', 'GetPlcRelease', 'UpdateTagType',
58          'AddSliceInstantiation', 'ResolveSlices', 'GetSlices',
59          'DeleteRoleFromPerson', 'GetSessions', 'UpdatePeer', 'VerifyPerson',
60          'GetPersonTags', 'DeleteKeyType', 'AddSlice', 'SliceUserAdd',
61          'DeleteSession', 'GetMessages', 'DeletePCU', 'GetPeerData',
62          'DeletePersonFromSite', 'DeleteTagType', 'GetPCUs', 'UpdateLeases',
63          'AddMessage', 'DeletePCUProtocolType', 'DeleteInterfaceTag',
64          'AddPersonToSite', 'GetSlivers', 'SliceNodesDel',
65          'DeleteAddressTypeFromAddress', 'AddNodeGroup', 'GetSliceTags',
66          'DeleteSite', 'GetSiteTags', 'UpdateMessage', 'DeleteSliceFromNodes',
67          'SliceRenew', 'UpdatePCUProtocolType', 'DeleteSiteTag',
68          'GetPCUProtocolTypes', 'GetEvents', 'GetSliceTicket', 'AddPersonTag',
69          'BootGetNodeDetails', 'DeleteInterface', 'DeleteNodeGroup',
70          'AddPCUProtocolType', 'BootCheckAuthentication', 'AddSiteTag',
71          'AddAddressTypeToAddress', 'DeleteConfFile', 'DeleteInitScript',
72          'DeletePerson', 'DeleteIlink', 'DeleteAddressType', 'AddBootState',
73          'AuthCheck', 'NotifySupport', 'GetSliceInstantiations', 'AddPCUType',
74          'AddPCU', 'AddSession', 'GetEventObjects', 'UpdateSiteTag', 
75          'UpdateNodeTag', 'AddPerson', 'BlacklistKey', 'UpdateInitScript',
76          'AddSliceToNodes', 'RebootNodeWithPCU', 'GetNodeTags', 'GetSliceKeys',
77          'GetSliceSshKeys', 'AddNetworkMethod', 'SliceNodesAdd',
78          'DeletePersonFromSlice', 'ReportRunlevel', 'GetNetworkTypes',
79          'UpdateSite', 'DeleteConfFileFromNodeGroup', 'UpdateNode',
80          'DeleteSliceInstantiation', 'DeleteSliceTag', 'BootUpdateNode',
81          'UpdatePerson', 'UpdateConfFile', 'SliceUserDel', 'DeleteLeases',
82          'AddConfFileToNodeGroup', 'UpdatePersonTag', 'DeleteConfFileFromNode',
83          'AddPersonToSlice', 'UnBindObjectFromPeer', 'AddNodeToPCU',
84          'GetLeaseGranularity', 'DeletePCUType', 'GetTagTypes', 'GetNodeTypes',
85          'UpdateInterfaceTag', 'GetRoles', 'UpdateSlice', 'UpdateSliceTag',
86          'AddSliceTag', 'AddNetworkType', 'AddInterface', 'AddAddressType',
87          'AddRoleToPerson', 'DeleteNodeType', 'GetLeases', 'UpdateInterface',
88          'SliceInfo', 'DeleteAddress', 'SliceTicketGet', 'GetPersons',
89          'GetWhitelist', 'AddKeyType', 'UpdateAddressType', 'GetPeerName',
90          'DeleteNetworkMethod', 'UpdateIlink', 'AddConfFileToNode', 'GetKeys',
91          'DeleteNodeFromPCU', 'GetInterfaceTags', 'GetBootStates',
92          'SetInterfaceSens', 'SetNodeLoadm', 'GetInterfaceRate', 'GetNodeLoadw',
93          'SetInterfaceKey', 'GetNodeSlices', 'GetNodeLoadm', 'SetSliceVref',
94          'GetInterfaceIwpriv', 'SetNodeLoadw', 'SetNodeSerial',
95          'GetNodePlainBootstrapfs', 'SetNodeMEMw', 'GetNodeResponse',
96          'SetInterfaceRate', 'SetSliceInitscript', 'SetNodeFcdistro',
97          'GetNodeLoady', 'SetNodeArch', 'SetNodeKargs', 'SetNodeMEMm',
98          'SetNodeBWy', 'SetNodeBWw', 'SetInterfaceSecurityMode', 'SetNodeBWm',
99          'SetNodeASType', 'GetNodeKargs', 'GetPersonColumnconf',
100          'GetNodeResponsem', 'GetNodeCPUy', 'GetNodeCramfs', 'SetNodeSlicesw',
101          'SetPersonColumnconf', 'SetNodeSlicesy', 'GetNodeCPUw', 'GetNodeBWy', 
102          'GetNodeCPUm', 'GetInterfaceDriver', 'GetNodeLoad', 'GetInterfaceMode',
103          'GetNodeSerial', 'SetNodeSlicesm', 'SetNodeLoady', 'GetNodeReliabilityw',
104          'SetSliceFcdistro', 'GetNodeReliabilityy', 'SetInterfaceEssid',
105          'SetSliceInitscriptCode', 'GetNodeExtensions', 'GetSliceOmfControl',
106          'SetNodeCity', 'SetInterfaceIfname', 'SetNodeHrn', 'SetNodeNoHangcheck', 
107          'GetNodeNoHangcheck', 'GetSliceFcdistro', 'SetNodeCountry',
108          'SetNodeKvariant', 'GetNodeKvariant', 'GetNodeMEMy', 'SetInterfaceIwpriv',
109          'GetNodeMEMw', 'SetInterfaceBackdoor', 'GetInterfaceFreq',
110          'SetInterfaceChannel', 'SetInterfaceNw', 'GetPersonShowconf',
111          'GetSliceInitscriptCode', 'SetNodeMEM', 'GetInterfaceEssid', 'GetNodeMEMm',
112          'SetInterfaceMode', 'SetInterfaceIwconfig', 'GetNodeSlicesm', 'GetNodeBWm',
113          'SetNodePlainBootstrapfs', 'SetNodeRegion', 'SetNodeCPU', 'GetNodeSlicesw',
114          'SetNodeBW', 'SetNodeSlices', 'SetNodeCramfs', 'GetNodeSlicesy',
115          'GetInterfaceKey', 'GetSliceInitscript', 'SetNodeCPUm', 'SetSliceArch',
116          'SetNodeLoad', 'SetNodeResponse', 'GetSliceSliverHMAC', 'GetNodeBWw',
117          'GetNodeRegion', 'SetNodeMEMy', 'GetNodeASType', 'SetNodePldistro',
118          'GetSliceArch', 'GetNodeCountry', 'SetSliceOmfControl', 'GetNodeHrn', 
119          'GetNodeCity', 'SetInterfaceAlias', 'GetNodeBW', 'GetNodePldistro',
120          'GetSlicePldistro', 'SetNodeASNumber', 'GetSliceHmac', 'SetSliceHmac',
121          'GetNodeMEM', 'GetNodeASNumber', 'GetInterfaceAlias', 'GetSliceVref',
122          'GetNodeArch', 'GetSliceSshKey', 'GetInterfaceKey4', 'GetInterfaceKey2',
123          'GetInterfaceKey3', 'GetInterfaceKey1', 'GetInterfaceBackdoor',
124          'GetInterfaceIfname', 'SetSliceSliverHMAC', 'SetNodeReliability',
125          'GetNodeCPU', 'SetPersonShowconf', 'SetNodeExtensions', 'SetNodeCPUy', 
126          'SetNodeCPUw', 'GetNodeResponsew', 'SetNodeResponsey', 'GetInterfaceSens',
127          'SetNodeResponsew', 'GetNodeResponsey', 'GetNodeReliability',
128          'GetNodeReliabilitym', 'SetNodeResponsem', 'SetInterfaceDriver',
129          'GetInterfaceSecurityMode', 'SetNodeDeployment', 'SetNodeReliabilitym',
130          'GetNodeFcdistro', 'SetInterfaceFreq', 'GetInterfaceNw',
131          'SetNodeReliabilityy', 'SetNodeReliabilityw', 'GetInterfaceIwconfig',
132          'SetSlicePldistro', 'SetSliceSshKey', 'GetNodeDeployment',
133          'GetInterfaceChannel', 'SetInterfaceKey2', 'SetInterfaceKey3',
134          'SetInterfaceKey1', 'SetInterfaceKey4'])
135      
136     _required_methods = set()
137
138     def __init__(self, username = None, password = None, session_key = None, 
139             proxy = None,
140             hostname = "www.planet-lab.eu",
141             urlpattern = "https://%(hostname)s:443/PLCAPI/",
142             local_peer = "PLE"):
143
144         self._blacklist = set()
145         self._reserved = set()
146         self._nodes_cache = None
147
148         if session_key is not None:
149             self.auth = dict(AuthMethod='session', session=session_key)
150         elif username is not None and password is not None:
151             self.auth = dict(AuthMethod='password', Username=username, AuthString=password)
152         else:
153             self.auth = dict(AuthMethod='anonymous')
154         
155         self._local_peer = local_peer
156         self._url = urlpattern % {'hostname':hostname}
157
158         if (proxy is not None):
159             import urllib2
160             class HTTPSProxyTransport(xmlrpclib.Transport):
161                 def __init__(self, proxy, use_datetime=0):
162                     opener = urllib2.build_opener(urllib2.ProxyHandler({"https" : proxy}))
163                     xmlrpclib.Transport.__init__(self, use_datetime)
164                     self.opener = opener
165
166                 def request(self, host, handler, request_body, verbose=0):
167                     req = urllib2.Request('https://%s%s' % (host, handler), request_body)
168                     req.add_header('User-agent', self.user_agent)
169                     self.verbose = verbose
170                     return self.parse_response(self.opener.open(req))
171
172             self._proxy_transport = lambda : HTTPSProxyTransport(proxy)
173         else:
174             self._proxy_transport = lambda : None
175         
176         self.threadlocal = threading.local()
177     
178     @property
179     def api(self):
180         # Cannot reuse same proxy in all threads, py2.7 is not threadsafe
181         return xmlrpclib.ServerProxy(
182             self._url ,
183             transport = self._proxy_transport(),
184             allow_none = True)
185         
186     @property
187     def mcapi(self):
188         try:
189             return self.threadlocal.mc
190         except AttributeError:
191             return self.api
192         
193     def test(self):
194         import warnings
195         
196         # validate XMLRPC server checking supported API calls
197         methods = set(_retry(self.mcapi.system.listMethods)())
198         if self._required_methods - methods:
199             warnings.warn("Unsupported REQUIRED methods: %s" % (
200                 ", ".join(sorted(self._required_methods - methods)), ) )
201             return False
202
203         if self._expected_methods - methods:
204             warnings.warn("Unsupported EXPECTED methods: %s" % (
205                 ", ".join(sorted(self._expected_methods - methods)), ) )
206         
207         try:
208             # test authorization
209             network_types = _retry(self.mcapi.GetNetworkTypes)(self.auth)
210         except (xmlrpclib.ProtocolError, xmlrpclib.Fault),e:
211             warnings.warn(str(e))
212         
213         return True
214     
215     @property
216     def network_types(self):
217         try:
218             return self._network_types
219         except AttributeError:
220             self._network_types = _retry(self.mcapi.GetNetworkTypes)(self.auth)
221             return self._network_types
222     
223     @property
224     def peer_map(self):
225         try:
226             return self._peer_map
227         except AttributeError:
228             peers = _retry(self.mcapi.GetPeers)(self.auth, {}, ['shortname','peername','peer_id'])
229             
230             self._peer_map = dict(
231                 (peer['shortname'], peer['peer_id'])
232                 for peer in peers
233             )
234
235             self._peer_map.update(
236                 (peer['peername'], peer['peer_id'])
237                 for peer in peers
238             )
239
240             self._peer_map.update(
241                 (peer['peer_id'], peer['shortname'])
242                 for peer in peers
243             )
244
245             self._peer_map[None] = self._local_peer
246             return self._peer_map
247
248     def get_node_flavour(self, node):
249         """
250         Returns detailed information on a given node's flavour,
251         i.e. its base installation.
252
253         This depends on the global PLC settings in the PLC_FLAVOUR area,
254         optionnally overridden by any of the following tags if set on that node:
255         'arch', 'pldistro', 'fcdistro', 'deployment', 'extensions'
256         
257         Params:
258         
259             * node : int or string
260                 - int, Node identifier
261                 - string, Fully qualified hostname
262         
263         Returns:
264
265             struct
266                 * extensions : array of string, extensions to add to the base install
267                 * fcdistro : string, the fcdistro this node should be based upon
268                 * nodefamily : string, the nodefamily this node should be based upon
269                 * plain : boolean, use plain bootstrapfs image if set (for tests)  
270         """
271         if not isinstance(node, (str, int, long)):
272             raise ValueError, "Node must be either a non-unicode string or an int"
273         return _retry(self.mcapi.GetNodeFlavour)(self.auth, node)
274     
275     def get_nodes(self, node_id_or_name = None, fields = None, **kw):
276         """
277         Returns an array of structs containing details about nodes. 
278         If node_id_or_name is specified and is an array of node identifiers
279         or hostnames,  or the filters keyword argument with struct of node
280         attributes, or node attributes by keyword argument,
281         only nodes matching the filter will be returned.
282
283         If fields is specified, only the specified details will be returned. 
284         NOTE that if fields is unspecified, the complete set of native fields are
285         returned, which DOES NOT include tags at this time.
286
287         Some fields may only be viewed by admins.
288         
289         Special params:
290             
291             fields: an optional list of fields to retrieve. The default is all.
292             
293             filters: an optional mapping with custom filters, which is the only
294                 way to support complex filters like negation and numeric comparisons.
295                 
296             peer: a string (or sequence of strings) with the name(s) of peers
297                 to filter - or None for local nodes.
298         """
299         if fields is not None:
300             fieldstuple = (fields,)
301         else:
302             fieldstuple = ()
303
304         if node_id_or_name is not None:
305             return _retry(self.mcapi.GetNodes)(self.auth, node_id_or_name, *fieldstuple)
306         else:
307             filters = kw.pop('filters',{})
308             
309             if 'peer' in kw:
310                 peer = kw.pop('peer')
311                 
312                 name_to_id = self.peer_map.get
313                 
314                 if hasattr(peer, '__iter__'):
315                     # we can't mix local and external nodes, so
316                     # split and re-issue recursively in that case
317                     if None in peer or self._local_peer in peer:
318                         if None in peer:    
319                             peer.remove(None)
320
321                         if self._local_peer in peer:
322                             peer.remove(self._local_peer)
323
324                         return (
325                             self.get_nodes(node_id_or_name, fields,
326                                     filters = filters, peer=peer, **kw) + \
327                             self.get_nodes(node_id_or_name, fields, 
328                                     filters = filters, peer=None, **kw)
329                          )
330                     else:
331                         peer_filter = map(name_to_id, peer)
332
333                 elif peer is None or peer == self._local_peer:
334                     peer_filter = None
335                 else:
336                     peer_filter = name_to_id(peer)
337                 
338                 filters['peer_id'] = peer_filter
339             
340             filters.update(kw)
341
342             if not filters and not fieldstuple:
343                 if not self._nodes_cache:
344                     self._nodes_cache = _retry(self.mcapi.GetNodes)(self.auth)
345                 return self._nodes_cache
346
347             return _retry(self.mcapi.GetNodes)(self.auth, filters, *fieldstuple)
348     
349     def get_node_tags(self, node_tag_id = None, fields = None, **kw):
350         if fields is not None:
351             fieldstuple = (fields,)
352         else:
353             fieldstuple = ()
354
355         if node_tag_id is not None:
356             return _retry(self.mcapi.GetNodeTags)(self.auth, node_tag_id,
357                     *fieldstuple)
358         else:
359             filters = kw.pop('filters',{})
360             filters.update(kw)
361             return _retry(self.mcapi.GetNodeTags)(self.auth, filters,
362                     *fieldstuple)
363
364     def get_slice_tags(self, slice_tag_id = None, fields = None, **kw):
365         if fields is not None:
366             fieldstuple = (fields,)
367         else:
368             fieldstuple = ()
369
370         if slice_tag_id is not None:
371             return _retry(self.mcapi.GetSliceTags)(self.auth, slice_tag_id,
372                     *fieldstuple)
373         else:
374             filters = kw.pop('filters',{})
375             filters.update(kw)
376             return _retry(self.mcapi.GetSliceTags)(self.auth, filters,
377                     *fieldstuple)
378     
379     def get_interfaces(self, interface_id_or_ip = None, fields = None, **kw):
380         if fields is not None:
381             fieldstuple = (fields,)
382         else:
383             fieldstuple = ()
384
385         if interface_id_or_ip is not None:
386             return _retry(self.mcapi.GetInterfaces)(self.auth,
387                     interface_id_or_ip, *fieldstuple)
388         else:
389             filters = kw.pop('filters',{})
390             filters.update(kw)
391             return _retry(self.mcapi.GetInterfaces)(self.auth, filters,
392                     *fieldstuple)
393         
394     def get_slices(self, slice_id_or_name = None, fields = None, **kw):
395         if fields is not None:
396             fieldstuple = (fields,)
397         else:
398             fieldstuple = ()
399
400         if slice_id_or_name is not None:
401             return _retry(self.mcapi.GetSlices)(self.auth, slice_id_or_name,
402                     *fieldstuple)
403         else:
404             filters = kw.pop('filters',{})
405             filters.update(kw)
406             return _retry(self.mcapi.GetSlices)(self.auth, filters,
407                     *fieldstuple)
408         
409     def update_slice(self, slice_id_or_name, **kw):
410         return _retry(self.mcapi.UpdateSlice)(self.auth, slice_id_or_name, kw)
411
412     def start_multicall(self):
413         self.threadlocal.mc = xmlrpclib.MultiCall(self.mcapi)
414     
415     def finish_multicall(self):
416         mc = self.threadlocal.mc
417         del self.threadlocal.mc
418         return _retry(mc)()
419
420     def get_slice_nodes(self, slicename):
421         return self.get_slices(slicename, ['node_ids'])[0]['node_ids']
422
423     def add_slice_nodes(self, slicename, nodes = None):
424         self.update_slice(slicename, nodes = nodes)
425
426     def get_node_info(self, node_id):
427         self.start_multicall()
428         info = self.get_nodes(node_id)
429         tags = self.get_node_tags(node_id=node_id, fields=('tagname','value'))
430         info, tags = self.finish_multicall()
431         return info, tags
432
433     def get_slice_id(self, slicename):
434         slice_id = None
435         slices = self.get_slices(slicename, fields=('slice_id',))
436         if slices:
437             slice_id = slices[0]['slice_id']
438
439         # If it wasn't found, don't remember this failure, keep trying
440         return slice_id
441
442     def get_slice_vnet_sys_tag(self, slicename):
443         slicetags = self.get_slice_tags(
444             name = slicename,
445             tagname = 'vsys_vnet',
446             fields=('value',))
447
448         if slicetags:
449             return slicetags[0]['value']
450         else:
451             return None
452
453     def blacklist_host(self, hostname):
454         self._blacklist.add(hostname)
455
456     def blacklisted(self):
457         return self._blacklist
458
459     def unblacklist_host(self, hostname):
460         del self._blacklist[hostname]
461
462     def reserve_host(self, hostname):
463         self._reserved.add(hostname)
464
465     def reserved(self):
466         return self._reserved
467
468     def unreserve_host(self, hostname):
469         del self._reserved[hostname]
470
471
472 class PLCAPIFactory(object):
473     """ 
474     .. note::
475
476         It allows PlanetLab RMs sharing a same slice, to use a same plcapi instance,
477         and to sincronize blacklisted and reserved hosts.
478
479     """
480     # use lock to avoid concurrent access to the Api list at the same times by 2 different threads
481     _lock = threading.Lock()
482     _apis = dict()
483
484     @classmethod 
485     def get_api(cls, slicename, pl_pass, pl_host,
486             pl_ptn = "https://%(hostname)s:443/PLCAPI/",
487             proxy = None):
488         """ Get existing PLCAPI instance
489
490         :param slicename: Planelab slice name
491         :type slicename: str
492         :param pl_pass: Planetlab password (used for web login)
493         :type pl_pass: str
494         :param pl_host: Planetlab registry host (e.g. "www.planet-lab.eu")
495         :type pl_host: str
496         :param pl_ptn: XMLRPC service pattern (e.g. https://%(hostname)s:443/PLCAPI/)
497         :type pl_ptn: str
498         :param proxy: Proxy service url
499         :type pl_ptn: str
500         """
501         if slice and pl_pass and pl_host:
502             key = cls._make_key(slicename, pl_host)
503             with cls._lock:
504                 api = cls._apis.get(key)
505                 if not api:
506                     api = cls.create_api(slicename, pl_pass, pl_host, pl_ptn, proxy)
507                 return api
508         return None
509
510     @classmethod 
511     def create_api(cls, slicename, pl_pass, pl_host,
512             pl_ptn = "https://%(hostname)s:443/PLCAPI/",
513             proxy = None):
514         """ Create an PLCAPI instance
515
516         :param slicename: Planelab slice name
517         :type slicename: str
518         :param pl_pass: Planetlab password (used for web login)
519         :type pl_pass: str
520         :param pl_host: Planetlab registry host (e.g. "www.planet-lab.eu")
521         :type pl_host: str
522         :param pl_ptn: XMLRPC service pattern (e.g. https://%(hostname)s:443/PLCAPI/)
523         :type pl_ptn: str
524         :param proxy: Proxy service url
525         :type pl_ptn: str
526         """
527         api = PLCAPI(
528             username = slicename,
529             password = pl_pass,
530             hostname = pl_host,
531             urlpattern = pl_ptn,
532             proxy = proxy
533         )
534         key = cls._make_key(slicename, pl_host)
535         cls._apis[key] = api
536         return api
537
538     @classmethod 
539     def _make_key(cls, *args):
540         """ Hash the credentials in order to create a key
541
542         :param args: list of arguments used to create the hash (user, host, port, ...)
543         :type args: list of args
544
545         """
546         skey = "".join(map(str, args))
547         return hashlib.md5(skey).hexdigest()
548