Merge "Sonar:Critical"
[so.git] / adapters / mso-adapter-utils / src / main / java / org / openecomp / mso / openstack / utils / MsoNeutronUtils.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP - SO
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.openecomp.mso.openstack.utils;
22
23
24 import java.io.Serializable;
25 import java.util.ArrayList;
26 import java.util.Calendar;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30
31 import org.openecomp.mso.cloud.CloudConfig;
32 import org.openecomp.mso.cloud.CloudConfigFactory;
33 import org.openecomp.mso.cloud.CloudIdentity;
34 import org.openecomp.mso.cloud.CloudSite;
35 import org.openecomp.mso.logger.MessageEnum;
36 import org.openecomp.mso.logger.MsoAlarmLogger;
37 import org.openecomp.mso.logger.MsoLogger;
38 import org.openecomp.mso.openstack.beans.NetworkInfo;
39 import org.openecomp.mso.openstack.exceptions.MsoAdapterException;
40 import org.openecomp.mso.openstack.exceptions.MsoCloudSiteNotFound;
41 import org.openecomp.mso.openstack.exceptions.MsoException;
42 import org.openecomp.mso.openstack.exceptions.MsoIOException;
43 import org.openecomp.mso.openstack.exceptions.MsoNetworkAlreadyExists;
44 import org.openecomp.mso.openstack.exceptions.MsoNetworkNotFound;
45 import org.openecomp.mso.openstack.exceptions.MsoOpenstackException;
46 import com.woorea.openstack.base.client.OpenStackBaseException;
47 import com.woorea.openstack.base.client.OpenStackConnectException;
48 import com.woorea.openstack.base.client.OpenStackRequest;
49 import com.woorea.openstack.base.client.OpenStackResponseException;
50 import com.woorea.openstack.keystone.Keystone;
51 import com.woorea.openstack.keystone.model.Access;
52 import com.woorea.openstack.keystone.utils.KeystoneUtils;
53 import com.woorea.openstack.quantum.Quantum;
54 import com.woorea.openstack.quantum.model.Network;
55 import com.woorea.openstack.quantum.model.Networks;
56 import com.woorea.openstack.quantum.model.Segment;
57 import com.woorea.openstack.keystone.model.Authentication;
58
59 public class MsoNeutronUtils extends MsoCommonUtils
60 {
61         // Cache Neutron Clients statically.  Since there is just one MSO user, there is no
62         // benefit to re-authentication on every request (or across different flows).  The
63         // token will be used until it expires.
64         //
65         // The cache key is "tenantId:cloudId"
66         private static Map<String,NeutronCacheEntry> neutronClientCache = new HashMap<String,NeutronCacheEntry>();
67
68         // Fetch cloud configuration each time (may be cached in CloudConfig class)
69         private CloudConfig cloudConfig;
70
71         private static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA);
72         private String msoPropID;
73
74         public enum NetworkType {
75                 BASIC, PROVIDER, MULTI_PROVIDER
76         };
77
78         public MsoNeutronUtils(String msoPropID, CloudConfigFactory cloudConfigFactory) {
79                 cloudConfig = cloudConfigFactory.getCloudConfig();
80                 this.msoPropID = msoPropID;
81         }
82
83         /**
84          * Create a network with the specified parameters in the given cloud/tenant.
85          *
86          * If a network already exists with the same name, an exception will be thrown.  Note that
87          * this is an MSO-imposed restriction.  Openstack does not require uniqueness on network names.
88          * <p>
89          * @param cloudSiteId The cloud identifier (may be a region) in which to create the network.
90          * @param tenantId The tenant in which to create the network
91          * @param type The type of network to create (Basic, Provider, Multi-Provider)
92          * @param networkName The network name to create
93          * @param provider The provider network name (for Provider or Multi-Provider networks)
94          * @param vlans A list of VLAN segments for the network (for Provider or Multi-Provider networks)
95          * @return a NetworkInfo object which describes the newly created network
96          * @throws MsoNetworkAlreadyExists Thrown if a network with the same name already exists
97          * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
98          * @throws MsoCloudSiteNotFound Thrown if the cloudSite is invalid or unknown
99          */
100         public NetworkInfo createNetwork (String cloudSiteId, String tenantId, NetworkType type, String networkName, String provider, List<Integer> vlans)
101             throws MsoException
102         {
103                 // Obtain the cloud site information where we will create the stack
104         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
105                 () -> new MsoCloudSiteNotFound(cloudSiteId));
106
107                 Quantum neutronClient = getNeutronClient (cloudSite, tenantId);
108
109                 // Check if a network already exists with this name
110                 // Openstack will allow duplicate name, so require explicit check
111                 Network network = findNetworkByName (neutronClient, networkName);
112
113                 if (network != null) {
114                         // Network already exists.  Throw an exception
115                         LOGGER.error(MessageEnum.RA_NETWORK_ALREADY_EXIST, networkName, cloudSiteId, tenantId, "Openstack", "", MsoLogger.ErrorCode.DataError, "Network already exists");
116                         throw new MsoNetworkAlreadyExists (networkName, tenantId, cloudSiteId);
117                 }
118
119                 // Does not exist, create a new one
120                 network = new Network();
121                 network.setName(networkName);
122                 network.setAdminStateUp(true);
123
124                 if (type == NetworkType.PROVIDER) {
125                         if (provider != null && vlans != null && vlans.size() > 0) {
126                                 network.setProviderPhysicalNetwork (provider);
127                                 network.setProviderNetworkType("vlan");
128                                 network.setProviderSegmentationId (vlans.get(0));
129                         }
130                 } else if (type == NetworkType.MULTI_PROVIDER) {
131                         if (provider != null && vlans != null && vlans.size() > 0) {
132                                 List<Segment> segments = new ArrayList<Segment>(vlans.size());
133                                 for (int vlan : vlans) {
134                                         Segment segment = new Segment();
135                                         segment.setProviderPhysicalNetwork (provider);
136                                         segment.setProviderNetworkType("vlan");
137                                         segment.setProviderSegmentationId (vlan);
138
139                                         segments.add(segment);
140                                 }
141                                 network.setSegments(segments);
142                         }
143                 }
144
145                 try {
146                         OpenStackRequest<Network> request = neutronClient.networks().create(network);
147                         Network newNetwork = executeAndRecordOpenstackRequest(request);
148                         return new NetworkInfo(newNetwork);
149                 }
150                 catch (OpenStackBaseException e) {
151                         // Convert Neutron exception to an MsoOpenstackException
152                         MsoException me = neutronExceptionToMsoException (e, "CreateNetwork");
153                         throw me;
154                 }
155                 catch (RuntimeException e) {
156                         // Catch-all
157                         MsoException me = runtimeExceptionToMsoException(e, "CreateNetwork");
158                         throw me;
159                 }
160         }
161
162
163         /**
164          * Query for a network with the specified name or ID in the given cloud.  If the network exists,
165          * return an NetworkInfo object.  If not, return null.
166          * <p>
167          * Whenever possible, the network ID should be used as it is much more efficient.  Query by
168          * name requires retrieval of all networks for the tenant and search for matching name.
169          * <p>
170          * @param networkNameOrId The network to query
171          * @param tenantId The Openstack tenant to look in for the network
172          * @param cloudSiteId The cloud identifier (may be a region) in which to query the network.
173          * @return a NetworkInfo object describing the queried network, or null if not found
174          * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
175          * @throws MsoCloudSiteNotFound
176          */
177     public NetworkInfo queryNetwork(String networkNameOrId, String tenantId, String cloudSiteId) throws MsoException
178         {
179                 LOGGER.debug("In queryNetwork");
180
181                 // Obtain the cloud site information
182         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
183                 () -> new MsoCloudSiteNotFound(cloudSiteId));
184
185                 Quantum neutronClient = getNeutronClient (cloudSite, tenantId);
186                 // Check if the network exists and return its info
187                 try {
188                         Network network = findNetworkByNameOrId (neutronClient, networkNameOrId);
189                         if (network == null) {
190                                 LOGGER.debug ("Query Network: " + networkNameOrId + " not found in tenant " + tenantId);
191                                 return null;
192                         }
193                         return new NetworkInfo(network);
194                 }
195                 catch (OpenStackBaseException e) {
196                         // Convert Neutron exception to an MsoOpenstackException
197                         MsoException me = neutronExceptionToMsoException (e, "QueryNetwork");
198                         throw me;
199                 }
200                 catch (RuntimeException e) {
201                         // Catch-all
202                         MsoException me = runtimeExceptionToMsoException(e, "QueryNetwork");
203                         throw me;
204                 }
205         }
206
207         /**
208          * Delete the specified Network (by ID) in the given cloud.
209          * If the network does not exist, success is returned.
210          * <p>
211          * @param networkId Openstack ID of the network to delete
212          * @param tenantId The Openstack tenant.
213          * @param cloudSiteId The cloud identifier (may be a region) from which to delete the network.
214          * @return true if the network was deleted, false if the network did not exist
215          * @throws MsoOpenstackException If the Openstack API call returns an exception, this local
216          * exception will be thrown.
217          * @throws MsoCloudSiteNotFound
218          */
219     public boolean deleteNetwork(String networkId, String tenantId, String cloudSiteId) throws MsoException
220         {
221                 // Obtain the cloud site information where we will create the stack
222         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
223                 () -> new MsoCloudSiteNotFound(cloudSiteId));
224                 Quantum neutronClient = getNeutronClient (cloudSite, tenantId);
225                 try {
226                         // Check that the network exists.
227                         Network network = findNetworkById (neutronClient, networkId);
228                         if (network == null) {
229                                 LOGGER.info(MessageEnum.RA_DELETE_NETWORK_EXC, networkId, cloudSiteId, tenantId, "Openstack", "");
230                                 return false;
231                         }
232
233                         OpenStackRequest<Void> request = neutronClient.networks().delete(network.getId());
234                         executeAndRecordOpenstackRequest(request);
235
236                         LOGGER.debug ("Deleted Network " + network.getId() + " (" + network.getName() + ")");
237                 }
238                 catch (OpenStackBaseException e) {
239                         // Convert Neutron exception to an MsoOpenstackException
240                         MsoException me = neutronExceptionToMsoException (e, "Delete Network");
241                         throw me;
242                 }
243                 catch (RuntimeException e) {
244                         // Catch-all
245                         MsoException me = runtimeExceptionToMsoException(e, "DeleteNetwork");
246                         throw me;
247                 }
248
249                 return true;
250         }
251
252
253         /**
254          * Update a network with the specified parameters in the given cloud/tenant.
255          *
256          * Specifically, this call is intended to update the VLAN segments on a
257          * multi-provider network.  The provider segments will be replaced with the
258          * supplied list of VLANs.
259          * <p>
260          * Note that updating the 'segments' array is not normally supported by Neutron.
261          * This method relies on a Platform Orchestration extension (using SDN controller
262          * to manage the virtual networking).
263          *
264          * @param cloudSiteId The cloud site ID (may be a region) in which to update the network.
265          * @param tenantId Openstack ID of the tenant in which to update the network
266          * @param networkId The unique Openstack ID of the network to be updated
267          * @param type The network type (Basic, Provider, Multi-Provider)
268          * @param provider The provider network name.  This should not change.
269          * @param vlans The list of VLAN segments to replace
270          * @return a NetworkInfo object which describes the updated network
271          * @throws MsoNetworkNotFound Thrown if the requested network does not exist
272          * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
273          * @throws MsoCloudSiteNotFound
274          */
275         public NetworkInfo updateNetwork (String cloudSiteId, String tenantId, String networkId, NetworkType type, String provider, List<Integer> vlans)
276             throws MsoException
277         {
278                 // Obtain the cloud site information where we will create the stack
279         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
280                 () -> new MsoCloudSiteNotFound(cloudSiteId));
281                 Quantum neutronClient = getNeutronClient (cloudSite, tenantId);
282                 // Check that the network exists
283                 Network network = findNetworkById (neutronClient, networkId);
284
285                 if (network == null) {
286                         // Network not found.  Throw an exception
287                         LOGGER.error(MessageEnum.RA_NETWORK_NOT_FOUND, networkId, cloudSiteId, tenantId, "Openstack", "", MsoLogger.ErrorCode.DataError, "Network not found");
288                         throw new MsoNetworkNotFound (networkId, tenantId, cloudSiteId);
289                 }
290
291                 // Overwrite the properties to be updated
292                 if (type == NetworkType.PROVIDER) {
293                         if (provider != null && vlans != null && vlans.size() > 0) {
294                                 network.setProviderPhysicalNetwork (provider);
295                                 network.setProviderNetworkType("vlan");
296                                 network.setProviderSegmentationId (vlans.get(0));
297                         }
298                 } else if (type == NetworkType.MULTI_PROVIDER) {
299                         if (provider != null && vlans != null && vlans.size() > 0) {
300                                 List<Segment> segments = new ArrayList<Segment>(vlans.size());
301                                 for (int vlan : vlans) {
302                                         Segment segment = new Segment();
303                                         segment.setProviderPhysicalNetwork (provider);
304                                         segment.setProviderNetworkType("vlan");
305                                         segment.setProviderSegmentationId (vlan);
306
307                                         segments.add(segment);
308                                 }
309                                 network.setSegments(segments);
310                         }
311                 }
312
313                 try {
314                         OpenStackRequest<Network> request = neutronClient.networks().update(network);
315                         Network newNetwork = executeAndRecordOpenstackRequest(request);
316                         return new NetworkInfo(newNetwork);
317                 }
318                 catch (OpenStackBaseException e) {
319                         // Convert Neutron exception to an MsoOpenstackException
320                         MsoException me = neutronExceptionToMsoException (e, "UpdateNetwork");
321                         throw me;
322                 }
323                 catch (RuntimeException e) {
324                         // Catch-all
325                         MsoException me = runtimeExceptionToMsoException(e, "UpdateNetwork");
326                         throw me;
327                 }
328         }
329
330
331         // -------------------------------------------------------------------
332         // PRIVATE UTILITY FUNCTIONS FOR USE WITHIN THIS CLASS
333
334         /**
335          * Get a Neutron (Quantum) client for the Openstack Network service.
336          * This requires a 'member'-level userId + password, which will be retrieved from
337          * properties based on the specified cloud Id.  The tenant in which to operate
338          * must also be provided.
339          * <p>
340          * On successful authentication, the Quantum object will be cached for the
341          * tenantID + cloudId so that it can be reused without reauthenticating with
342          *  Openstack every time.
343          *
344          * @param cloudSite - a cloud site definition
345          * @param tenantId - Openstack tenant ID
346          * @return an authenticated Quantum object
347          */
348     private Quantum getNeutronClient(CloudSite cloudSite, String tenantId) throws MsoException
349         {
350                 String cloudId = cloudSite.getId();
351
352                 // Check first in the cache of previously authorized clients
353                 String cacheKey = cloudId + ":" + tenantId;
354                 if (neutronClientCache.containsKey(cacheKey)) {
355                         if (! neutronClientCache.get(cacheKey).isExpired()) {
356                                 LOGGER.debug ("Using Cached HEAT Client for " + cacheKey);
357                                 Quantum neutronClient = neutronClientCache.get(cacheKey).getNeutronClient();
358                                 return neutronClient;
359                         }
360                         else {
361                                 // Token is expired.  Remove it from cache.
362                                 neutronClientCache.remove(cacheKey);
363                                 LOGGER.debug ("Expired Cached Neutron Client for " + cacheKey);
364                         }
365                 }
366
367                 // Obtain an MSO token for the tenant from the identity service
368                 CloudIdentity cloudIdentity = cloudSite.getIdentityService();
369                 Keystone keystoneTenantClient = new Keystone (cloudIdentity.getKeystoneUrl(cloudId, msoPropID));
370                 Access access = null;
371                 try {
372                         Authentication credentials = cloudIdentity.getAuthentication ();
373                         OpenStackRequest<Access> request = keystoneTenantClient.tokens().authenticate(credentials).withTenantId(tenantId);
374                         access = executeAndRecordOpenstackRequest(request);
375                 }
376                 catch (OpenStackResponseException e) {
377                         if (e.getStatus() == 401) {
378                                 // Authentication error.
379                                 String error = "Authentication Failure: tenant=" + tenantId + ",cloud=" + cloudIdentity.getId();
380                                 alarmLogger .sendAlarm("MsoAuthenticationError", MsoAlarmLogger.CRITICAL, error);
381                                 throw new MsoAdapterException(error);
382                         }
383                         else {
384                                 MsoException me = keystoneErrorToMsoException(e, "TokenAuth");
385                                 throw me;
386                         }
387                 }
388                 catch (OpenStackConnectException e) {
389                         // Connection to Openstack failed
390                         MsoIOException me = new MsoIOException (e.getMessage(), e);
391                         me.addContext("TokenAuth");
392                         throw me;
393                 }
394                 catch (RuntimeException e) {
395                         // Catch-all
396                         MsoException me = runtimeExceptionToMsoException(e, "TokenAuth");
397                         throw me;
398                 }
399
400                 String region = cloudSite.getRegionId();
401                 String neutronUrl = null;
402                 try {
403                         neutronUrl = KeystoneUtils.findEndpointURL(access.getServiceCatalog(), "network", region, "public");
404                         if (! neutronUrl.endsWith("/")) {
405                 neutronUrl += "/v2.0/";
406             }
407                 } catch (RuntimeException e) {
408                         // This comes back for not found (probably an incorrect region ID)
409                         String error = "Network service not found: region=" + region + ",cloud=" + cloudIdentity.getId();
410                         alarmLogger.sendAlarm("MsoConfigurationError", MsoAlarmLogger.CRITICAL, error);
411                         throw new MsoAdapterException (error, e);
412                 }
413
414                 Quantum neutronClient = new Quantum(neutronUrl);
415                 neutronClient.token(access.getToken().getId());
416
417                 neutronClientCache.put(cacheKey, new NeutronCacheEntry(neutronUrl, access.getToken().getId(), access.getToken().getExpires()));
418                 LOGGER.debug ("Caching Neutron Client for " + cacheKey);
419
420                 return neutronClient;
421         }
422
423         /**
424          * Forcibly expire a Neutron client from the cache.  This call is for use by
425          * the KeystoneClient in case where a tenant is deleted.  In that case,
426          * all cached credentials must be purged so that fresh authentication is
427          * done on subsequent calls.
428          */
429         public static void expireNeutronClient (String tenantId, String cloudId) {
430                 String cacheKey = cloudId + ":" + tenantId;
431                 if (neutronClientCache.containsKey(cacheKey)) {
432                         neutronClientCache.remove(cacheKey);
433                         LOGGER.debug ("Deleted Cached Neutron Client for " + cacheKey);
434                 }
435         }
436
437
438         /*
439          * Find a tenant (or query its existence) by its Name or Id.  Check first against the
440          * ID.  If that fails, then try by name.
441          *
442          * @param adminClient an authenticated Keystone object
443          * @param tenantName the tenant name or ID to query
444          * @return a Tenant object or null if not found
445          */
446         public Network findNetworkByNameOrId (Quantum neutronClient, String networkNameOrId)
447         {
448                 if (networkNameOrId == null) {
449             return null;
450         }
451
452                 Network network = findNetworkById(neutronClient, networkNameOrId);
453
454                 if (network == null) {
455             network = findNetworkByName(neutronClient, networkNameOrId);
456         }
457
458                 return network;
459         }
460
461         /*
462          * Find a network (or query its existence) by its Id.
463          *
464          * @param neutronClient an authenticated Quantum object
465          * @param networkId the network ID to query
466          * @return a Network object or null if not found
467          */
468         private static Network findNetworkById (Quantum neutronClient, String networkId)
469         {
470                 if (networkId == null) {
471             return null;
472         }
473
474                 try {
475                         OpenStackRequest<Network> request = neutronClient.networks().show(networkId);
476                         Network network = executeAndRecordOpenstackRequest(request);
477                         return network;
478                 }
479                 catch (OpenStackResponseException e) {
480                         if (e.getStatus() == 404) {
481                                 return null;
482                         } else {
483                                 LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "Openstack Error, GET Network By ID (" + networkId + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack");
484                                 throw e;
485                         }
486                 }
487         }
488
489         /*
490          * Find a network (or query its existence) by its Name.  This method avoids an
491          * initial lookup by ID when it's known that we have the network Name.
492          *
493          * Neutron does not support 'name=*' query parameter for Network query (show).
494          * The only way to query by name is to retrieve all networks and look for the
495          * match.  While inefficient, this capability will be provided as it is needed
496          * by MSO, but should be avoided in favor of ID whenever possible.
497          *
498          * TODO:
499          * Network names are not required to be unique, though MSO will attempt to enforce
500          * uniqueness.  This call probably needs to return an error (instead of returning
501          * the first match).
502          *
503          * @param neutronClient an authenticated Quantum object
504          * @param networkName the network name to query
505          * @return a Network object or null if not found
506          */
507         public Network findNetworkByName (Quantum neutronClient, String networkName)
508         {
509                 if (networkName == null) {
510             return null;
511         }
512
513                 try {
514                         OpenStackRequest<Networks> request = neutronClient.networks().list();
515                         Networks networks = executeAndRecordOpenstackRequest(request);
516                         for (Network network : networks.getList()) {
517                                 if (network.getName().equals(networkName)) {
518                                         LOGGER.debug ("Found match on network name: " + networkName);
519                                         return network;
520                                 }
521                         }
522                         LOGGER.debug ("findNetworkByName - no match found for " + networkName);
523                         return null;
524                 }
525                 catch (OpenStackResponseException e) {
526                         if (e.getStatus() == 404) {
527                                 return null;
528                         } else {
529                                 LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "Openstack Error, GET Network By Name (" + networkName + "): " + e, "OpenStack", "", MsoLogger.ErrorCode.DataError, "Exception in OpenStack");
530                                 throw e;
531                         }
532                 }
533         }
534
535
536         /*
537          * An entry in the Neutron Client Cache.  It saves the Neutron client object
538          * along with the token expiration.  After this interval, this cache
539          * item will no longer be used.
540          */
541         private static class NeutronCacheEntry implements Serializable
542         {
543                 private static final long serialVersionUID = 1L;
544
545                 private String neutronUrl;
546                 private String token;
547                 private Calendar expires;
548
549                 public NeutronCacheEntry (String neutronUrl, String token, Calendar expires) {
550                         this.neutronUrl = neutronUrl;
551                         this.token = token;
552                         this.expires = expires;
553                 }
554
555                 public Quantum getNeutronClient () {
556                         Quantum neutronClient = new Quantum(neutronUrl);
557                         neutronClient.token(token);
558                         return neutronClient;
559                 }
560
561                 public boolean isExpired() {
562                         if (expires == null) {
563                 return true;
564             }
565
566             return System.currentTimeMillis() > expires.getTimeInMillis();
567                 }
568         }
569
570         /**
571          * Clean up the Neutron client cache to remove expired entries.
572          */
573         public static void neutronCacheCleanup () {
574                 for (String cacheKey : neutronClientCache.keySet()) {
575                         if (neutronClientCache.get(cacheKey).isExpired()) {
576                                 neutronClientCache.remove(cacheKey);
577                                 LOGGER.debug ("Cleaned Up Cached Neutron Client for " + cacheKey);
578                         }
579                 }
580         }
581
582         /**
583          * Reset the Neutron client cache.
584          * This may be useful if cached credentials get out of sync.
585          */
586         public static void neutronCacheReset () {
587                 neutronClientCache = new HashMap<>();
588         }
589 }