merged ex_shutdown into nepi-3-dev
[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 delete_slice_node(self, slice_id_or_name, node_id_or_hostname):
413         return _retry(self.mcapi.DeleteSliceFromNodes)(self.auth, slice_id_or_name, node_id_or_hostname)
414
415     def start_multicall(self):
416         self.threadlocal.mc = xmlrpclib.MultiCall(self.mcapi)
417     
418     def finish_multicall(self):
419         mc = self.threadlocal.mc
420         del self.threadlocal.mc
421         return _retry(mc)()
422
423     def get_slice_nodes(self, slicename):
424         return self.get_slices(slicename, ['node_ids'])[0]['node_ids']
425
426     def add_slice_nodes(self, slicename, nodes = None):
427         self.update_slice(slicename, nodes = nodes)
428
429     def get_node_info(self, node_id):
430         self.start_multicall()
431         info = self.get_nodes(node_id)
432         tags = self.get_node_tags(node_id=node_id, fields=('tagname','value'))
433         info, tags = self.finish_multicall()
434         return info, tags
435
436     def get_slice_id(self, slicename):
437         slice_id = None
438         slices = self.get_slices(slicename, fields=('slice_id',))
439         if slices:
440             slice_id = slices[0]['slice_id']
441
442         # If it wasn't found, don't remember this failure, keep trying
443         return slice_id
444
445     def get_slice_vnet_sys_tag(self, slicename):
446         slicetags = self.get_slice_tags(
447             name = slicename,
448             tagname = 'vsys_vnet',
449             fields=('value',))
450
451         if slicetags:
452             return slicetags[0]['value']
453         else:
454             return None
455
456     def blacklist_host(self, hostname):
457         self._blacklist.add(hostname)
458
459     def blacklisted(self):
460         return self._blacklist
461
462     def unblacklist_host(self, hostname):
463         del self._blacklist[hostname]
464
465     def reserve_host(self, hostname):
466         self._reserved.add(hostname)
467
468     def reserved(self):
469         return self._reserved
470
471     def unreserve_host(self, hostname):
472         del self._reserved[hostname]
473
474
475 class PLCAPIFactory(object):
476     """ 
477     .. note::
478
479         It allows PlanetLab RMs sharing a same slice, to use a same plcapi instance,
480         and to sincronize blacklisted and reserved hosts.
481
482     """
483     # use lock to avoid concurrent access to the Api list at the same times by 2 different threads
484     _lock = threading.Lock()
485     _apis = dict()
486
487     @classmethod 
488     def get_api(cls, pl_user, pl_pass, pl_host,
489             pl_ptn = "https://%(hostname)s:443/PLCAPI/",
490             proxy = None):
491         """ Get existing PLCAPI instance
492
493         :param pl_user: Planelab user name (used for web login)
494         :type pl_user: str
495         :param pl_pass: Planetlab password (used for web login)
496         :type pl_pass: str
497         :param pl_host: Planetlab registry host (e.g. "www.planet-lab.eu")
498         :type pl_host: str
499         :param pl_ptn: XMLRPC service pattern (e.g. https://%(hostname)s:443/PLCAPI/)
500         :type pl_ptn: str
501         :param proxy: Proxy service url
502         :type pl_ptn: str
503         """
504         if pl_user and pl_pass and pl_host:
505             key = cls._make_key(pl_user, pl_host)
506             with cls._lock:
507                 api = cls._apis.get(key)
508                 if not api:
509                     api = cls.create_api(pl_user, pl_pass, pl_host, pl_ptn, proxy)
510                 return api
511         return None
512
513     @classmethod 
514     def create_api(cls, pl_user, pl_pass, pl_host,
515             pl_ptn = "https://%(hostname)s:443/PLCAPI/",
516             proxy = None):
517         """ Create an PLCAPI instance
518
519         :param pl_user: Planelab user name (used for web login)
520         :type pl_user: str
521         :param pl_pass: Planetlab password (used for web login)
522         :type pl_pass: str
523         :param pl_host: Planetlab registry host (e.g. "www.planet-lab.eu")
524         :type pl_host: str
525         :param pl_ptn: XMLRPC service pattern (e.g. https://%(hostname)s:443/PLCAPI/)
526         :type pl_ptn: str
527         :param proxy: Proxy service url
528         :type pl_ptn: str
529         """
530         api = PLCAPI(
531             username = pl_user,
532             password = pl_pass,
533             hostname = pl_host,
534             urlpattern = pl_ptn,
535             proxy = proxy
536         )
537         key = cls._make_key(pl_user, pl_host)
538         cls._apis[key] = api
539         return api
540
541     @classmethod 
542     def _make_key(cls, *args):
543         """ Hash the credentials in order to create a key
544
545         :param args: list of arguments used to create the hash (user, host, port, ...)
546         :type args: list of args
547
548         """
549         skey = "".join(map(str, args))
550         return hashlib.md5(skey).hexdigest()
551