3b710032c1919d5d933c87c84435c132299cd1db
[so.git] / adapters / mso-adapter-utils / src / main / java / org / openecomp / mso / openstack / utils / MsoKeystoneUtils.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.Calendar;
26 import java.util.HashMap;
27 import java.util.Map;
28
29 import java.util.Optional;
30 import org.openecomp.mso.cloud.CloudIdentity;
31 import org.openecomp.mso.cloud.CloudSite;
32 import org.openecomp.mso.logger.MsoAlarmLogger;
33 import org.openecomp.mso.logger.MsoLogger;
34 import org.openecomp.mso.logger.MessageEnum;
35 import org.openecomp.mso.openstack.beans.MsoTenant;
36 import org.openecomp.mso.openstack.exceptions.MsoAdapterException;
37 import org.openecomp.mso.openstack.exceptions.MsoCloudSiteNotFound;
38 import org.openecomp.mso.openstack.exceptions.MsoException;
39 import org.openecomp.mso.openstack.exceptions.MsoOpenstackException;
40 import org.openecomp.mso.openstack.exceptions.MsoTenantAlreadyExists;
41 import com.woorea.openstack.base.client.OpenStackBaseException;
42 import com.woorea.openstack.base.client.OpenStackConnectException;
43 import com.woorea.openstack.base.client.OpenStackRequest;
44 import com.woorea.openstack.base.client.OpenStackResponseException;
45 import com.woorea.openstack.keystone.Keystone;
46 import com.woorea.openstack.keystone.model.Access;
47 import com.woorea.openstack.keystone.model.Metadata;
48 import com.woorea.openstack.keystone.model.Role;
49 import com.woorea.openstack.keystone.model.Roles;
50 import com.woorea.openstack.keystone.model.Tenant;
51 import com.woorea.openstack.keystone.model.User;
52 import com.woorea.openstack.keystone.utils.KeystoneUtils;
53 import com.woorea.openstack.keystone.model.Authentication;
54
55 public class MsoKeystoneUtils extends MsoTenantUtils {
56
57     // Cache the Keystone Clients statically. Since there is just one MSO user, there is no
58     // benefit to re-authentication on every request (or across different flows). The
59     // token will be used until it expires.
60     //
61     // The cache key is "cloudId"
62     private static Map <String, KeystoneCacheEntry> adminClientCache = new HashMap<>();
63
64         private static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA);
65         String msoPropID;
66         
67     public MsoKeystoneUtils (String msoPropID) {
68                 super(msoPropID);
69                 this.msoPropID = msoPropID;
70                 LOGGER.debug("MsoKeyStoneUtils:" + msoPropID);
71         }
72
73     /**
74      * Create a tenant with the specified name in the given cloud. If the tenant already exists,
75      * an Exception will be thrown. The MSO User will also be added to the "member" list of
76      * the new tenant to perform subsequent Nova/Heat commands in the tenant. If the MSO User
77      * association fails, the entire transaction will be rolled back.
78      * <p>
79      * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin
80      * requests go to the centralized identity service in DCP. However, if some artifact
81      * must exist in each local LCP instance as well, then it will be needed to access the
82      * correct region.
83      * <p>
84      *
85      * @param tenantName The tenant name to create
86      * @param cloudSiteId The cloud identifier (may be a region) in which to create the tenant.
87      * @return the tenant ID of the newly created tenant
88      * @throws MsoTenantAlreadyExists Thrown if the requested tenant already exists
89      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
90      */
91     public String createTenant (String tenantName,
92                                 String cloudSiteId,
93                                 Map <String, String> metadata,
94                                 boolean backout) throws MsoException {
95         // Obtain the cloud site information where we will create the tenant
96         Optional<CloudSite> cloudSiteOpt = cloudConfig.getCloudSite(cloudSiteId);
97         if (!cloudSiteOpt.isPresent()) {
98                 LOGGER.error(MessageEnum.RA_CREATE_TENANT_ERR, "MSOCloudSite not found", "", "", MsoLogger.ErrorCode.DataError, "MSOCloudSite not found");
99             throw new MsoCloudSiteNotFound (cloudSiteId);
100         }
101         Keystone keystoneAdminClient = getKeystoneAdminClient(cloudSiteOpt.get());
102         Tenant tenant = null;
103         try {
104             // Check if the tenant already exists
105             tenant = findTenantByName (keystoneAdminClient, tenantName);
106
107             if (tenant != null) {
108                 // Tenant already exists. Throw an exception
109                 LOGGER.error(MessageEnum.RA_TENANT_ALREADY_EXIST, tenantName, cloudSiteId, "", "", MsoLogger.ErrorCode.DataError, "Tenant already exists");
110                 throw new MsoTenantAlreadyExists (tenantName, cloudSiteId);
111             }
112
113             // Does not exist, create a new one
114             tenant = new Tenant ();
115             tenant.setName (tenantName);
116             tenant.setDescription ("SDN Tenant (via MSO)");
117             tenant.setEnabled (true);
118
119             OpenStackRequest <Tenant> request = keystoneAdminClient.tenants ().create (tenant);
120             tenant = executeAndRecordOpenstackRequest (request, msoProps);
121         } catch (OpenStackBaseException e) {
122             // Convert Keystone OpenStackResponseException to MsoOpenstackException
123             throw keystoneErrorToMsoException (e, "CreateTenant");
124         } catch (RuntimeException e) {
125             // Catch-all
126             throw runtimeExceptionToMsoException (e, "CreateTenant");
127         }
128
129         // Add MSO User to the tenant as a member and
130         // apply tenant metadata if supported by the cloud site
131         try {
132             CloudIdentity cloudIdentity = cloudSiteOpt.get().getIdentityService ();
133
134             User msoUser = findUserByNameOrId (keystoneAdminClient, cloudIdentity.getMsoId ());
135             Role memberRole = findRoleByNameOrId (keystoneAdminClient, cloudIdentity.getMemberRole ());
136
137             OpenStackRequest <Void> request = keystoneAdminClient.tenants ().addUser (tenant.getId (),
138                                                                                       msoUser.getId (),
139                                                                                       memberRole.getId ());
140             executeAndRecordOpenstackRequest (request, msoProps);
141
142             if (cloudIdentity.hasTenantMetadata () && metadata != null && !metadata.isEmpty ()) {
143                 Metadata tenantMetadata = new Metadata ();
144                 tenantMetadata.setMetadata (metadata);
145
146                 OpenStackRequest <Metadata> metaRequest = keystoneAdminClient.tenants ()
147                                                                              .createOrUpdateMetadata (tenant.getId (),
148                                                                                                       tenantMetadata);
149                 executeAndRecordOpenstackRequest (metaRequest, msoProps);
150             }
151         } catch (Exception e) {
152             // Failed to attach MSO User to the new tenant. Can't operate without access,
153             // so roll back the tenant.
154                 if (!backout)
155                 {
156                         LOGGER.warn(MessageEnum.RA_CREATE_TENANT_ERR, "Create Tenant errored, Tenant deletion suppressed", "Openstack", "", MsoLogger.ErrorCode.DataError, "Create Tenant error, Tenant deletion suppressed");
157                 }
158                 else
159                 {
160                         try {
161                                 OpenStackRequest <Void> request = keystoneAdminClient.tenants ().delete (tenant.getId ());
162                                 executeAndRecordOpenstackRequest (request, msoProps);
163                         } catch (Exception e2) {
164                                 // Just log this one. We will report the original exception.
165                                 LOGGER.error (MessageEnum.RA_CREATE_TENANT_ERR, "Nested exception rolling back tenant", "Openstack", "", MsoLogger.ErrorCode.DataError, "Create Tenant error, Nested exception rolling back tenant", e2);
166                         }
167                 }
168                 
169
170             // Propagate the original exception on user/role/tenant mapping
171             if (e instanceof OpenStackBaseException) {
172                 // Convert Keystone Exception to MsoOpenstackException
173                 throw keystoneErrorToMsoException ((OpenStackBaseException) e, "CreateTenantUser");
174             } else {
175                 MsoAdapterException me = new MsoAdapterException (e.getMessage (), e);
176                 me.addContext ("CreateTenantUser");
177                 throw me;
178             }
179         }
180         return tenant.getId ();
181     }
182
183     /**
184      * Query for a tenant by ID in the given cloud. If the tenant exists,
185      * return an MsoTenant object. If not, return null.
186      * <p>
187      * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin
188      * requests go to the centralized identity service in DCP. However, if some artifact
189      * must exist in each local LCP instance as well, then it will be needed to access the
190      * correct region.
191      * <p>
192      *
193      * @param tenantId The Openstack ID of the tenant to query
194      * @param cloudSiteId The cloud identifier (may be a region) in which to query the tenant.
195      * @return the tenant properties of the queried tenant, or null if not found
196      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
197      */
198     public MsoTenant queryTenant (String tenantId, String cloudSiteId) throws MsoException {
199         // Obtain the cloud site information where we will query the tenant
200         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
201                 () -> new MsoCloudSiteNotFound(cloudSiteId));
202
203         Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite);
204
205         // Check if the tenant exists and return its Tenant Id
206         try {
207             Tenant tenant = findTenantById (keystoneAdminClient, tenantId);
208             if (tenant == null) {
209                 return null;
210             }
211
212             Map <String, String> metadata = new HashMap<>();
213             if (cloudSite.getIdentityService ().hasTenantMetadata ()) {
214                 OpenStackRequest <Metadata> request = keystoneAdminClient.tenants ().showMetadata (tenant.getId ());
215                 Metadata tenantMetadata = executeAndRecordOpenstackRequest (request, msoProps);
216                 if (tenantMetadata != null) {
217                     metadata = tenantMetadata.getMetadata ();
218                 }
219             }
220             return new MsoTenant (tenant.getId (), tenant.getName (), metadata);
221         } catch (OpenStackBaseException e) {
222             // Convert Keystone OpenStackResponseException to MsoOpenstackException
223             throw keystoneErrorToMsoException (e, "QueryTenant");
224         } catch (RuntimeException e) {
225             // Catch-all
226             throw runtimeExceptionToMsoException (e, "QueryTenant");
227         }
228     }
229
230     /**
231      * Query for a tenant with the specified name in the given cloud. If the tenant exists,
232      * return an MsoTenant object. If not, return null. This query is useful if the client
233      * knows it has the tenant name, skipping an initial lookup by ID that would always fail.
234      * <p>
235      * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin
236      * requests go to the centralized identity service in DCP. However, if some artifact
237      * must exist in each local LCP instance as well, then it will be needed to access the
238      * correct region.
239      * <p>
240      *
241      * @param tenantName The name of the tenant to query
242      * @param cloudSiteId The cloud identifier (may be a region) in which to query the tenant.
243      * @return the tenant properties of the queried tenant, or null if not found
244      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
245      */
246     public MsoTenant queryTenantByName (String tenantName, String cloudSiteId) throws MsoException {
247         // Obtain the cloud site information where we will query the tenant
248         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
249                 () -> new MsoCloudSiteNotFound(cloudSiteId));
250         Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite);
251
252         try {
253             Tenant tenant = findTenantByName (keystoneAdminClient, tenantName);
254             if (tenant == null) {
255                 return null;
256             }
257
258             Map <String, String> metadata = new HashMap<>();
259             if (cloudSite.getIdentityService ().hasTenantMetadata ()) {
260                 OpenStackRequest <Metadata> request = keystoneAdminClient.tenants ().showMetadata (tenant.getId ());
261                 Metadata tenantMetadata = executeAndRecordOpenstackRequest (request, msoProps);
262                 if (tenantMetadata != null) {
263                     metadata = tenantMetadata.getMetadata ();
264                 }
265             }
266             return new MsoTenant (tenant.getId (), tenant.getName (), metadata);
267         } catch (OpenStackBaseException e) {
268             // Convert Keystone OpenStackResponseException to MsoOpenstackException
269             throw keystoneErrorToMsoException (e, "QueryTenantName");
270         } catch (RuntimeException e) {
271             // Catch-all
272             throw runtimeExceptionToMsoException (e, "QueryTenantName");
273         }
274     }
275
276     /**
277      * Delete the specified Tenant (by ID) in the given cloud. This method returns true or
278      * false, depending on whether the tenant existed and was successfully deleted, or if
279      * the tenant already did not exist. Both cases are treated as success (no Exceptions).
280      * <p>
281      * Note for the AIC Cloud (DCP/LCP): all admin requests go to the centralized identity
282      * service in DCP. So deleting a tenant from one cloudSiteId will remove it from all
283      * sites managed by that identity service.
284      * <p>
285      *
286      * @param tenantId The Openstack ID of the tenant to delete
287      * @param cloudSiteId The cloud identifier from which to delete the tenant.
288      * @return true if the tenant was deleted, false if the tenant did not exist.
289      * @throws MsoOpenstackException If the Openstack API call returns an exception.
290      */
291     public boolean deleteTenant (String tenantId, String cloudSiteId) throws MsoException {
292         // Obtain the cloud site information where we will query the tenant
293         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
294                 () -> new MsoCloudSiteNotFound(cloudSiteId));
295         Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite);
296
297         try {
298             // Check that the tenant exists. Also, need the ID to delete
299             Tenant tenant = findTenantById (keystoneAdminClient, tenantId);
300             if (tenant == null) {
301                 LOGGER.error(MessageEnum.RA_TENANT_NOT_FOUND, tenantId, cloudSiteId, "", "", MsoLogger.ErrorCode.DataError, "Tenant not found");
302                 return false;
303             }
304
305             OpenStackRequest <Void> request = keystoneAdminClient.tenants ().delete (tenant.getId ());
306             executeAndRecordOpenstackRequest (request, msoProps);
307             LOGGER.debug ("Deleted Tenant " + tenant.getId () + " (" + tenant.getName () + ")");
308
309             // Clear any cached clients. Not really needed, ID will not be reused.
310             MsoHeatUtils.expireHeatClient (tenant.getId (), cloudSiteId);
311             MsoNeutronUtils.expireNeutronClient (tenant.getId (), cloudSiteId);
312         } catch (OpenStackBaseException e) {
313             // Convert Keystone OpenStackResponseException to MsoOpenstackException
314             throw keystoneErrorToMsoException (e, "Delete Tenant");
315         } catch (RuntimeException e) {
316             // Catch-all
317             throw runtimeExceptionToMsoException (e, "DeleteTenant");
318         }
319
320         return true;
321     }
322
323     // -------------------------------------------------------------------
324     // PRIVATE UTILITY FUNCTIONS FOR USE WITHIN THIS CLASS
325
326     /*
327      * Get a Keystone Admin client for the Openstack Identity service.
328      * This requires an 'admin'-level userId + password along with an 'admin' tenant
329      * in the target cloud. These values will be retrieved from properties based
330      * on the specified cloud ID.
331      * <p>
332      * On successful authentication, the Keystone object will be cached for the cloudId
333      * so that it can be reused without going back to Openstack every time.
334      *
335      * @param cloudId
336      *
337      * @return an authenticated Keystone object
338      */
339     public Keystone getKeystoneAdminClient (CloudSite cloudSite) throws MsoException {
340         CloudIdentity cloudIdentity = cloudSite.getIdentityService ();
341
342         String cloudId = cloudIdentity.getId ();
343         String adminTenantName = cloudIdentity.getAdminTenant ();
344         String region = cloudSite.getRegionId ();
345
346         // Check first in the cache of previously authorized clients
347         KeystoneCacheEntry entry = adminClientCache.get (cloudId);
348         if (entry != null) {
349             if (!entry.isExpired ()) {
350                 return entry.getKeystoneClient ();
351             } else {
352                 // Token is expired. Remove it from cache.
353                 adminClientCache.remove (cloudId);
354             }
355         }
356
357         Keystone keystone = new Keystone (cloudIdentity.getKeystoneUrl (region, msoPropID));
358
359         // Must authenticate against the 'admin' tenant to get the services endpoints
360         Access access = null;
361         String token = null;
362         try {
363                 Authentication credentials = cloudIdentity.getAuthentication ();
364             OpenStackRequest <Access> request = keystone.tokens ()
365                                                         .authenticate (credentials)
366                                                         .withTenantName (adminTenantName);
367             access = executeAndRecordOpenstackRequest (request, msoProps);
368             token = access.getToken ().getId ();
369         } catch (OpenStackResponseException e) {
370             if (e.getStatus () == 401) {
371                 // Authentication error. Can't access admin tenant - something is mis-configured
372                 String error = "MSO Authentication Failed for " + cloudIdentity.getId ();
373                 alarmLogger.sendAlarm ("MsoAuthenticationError", MsoAlarmLogger.CRITICAL, error);
374                 throw new MsoAdapterException (error);
375             } else {
376                 throw keystoneErrorToMsoException (e, "TokenAuth");
377             }
378         } catch (OpenStackConnectException e) {
379             // Connection to Openstack failed
380             throw keystoneErrorToMsoException (e, "TokenAuth");
381         }
382
383         // Get the Identity service URL. Throws runtime exception if not found per region.
384         String adminUrl = null;
385         try {
386             adminUrl = KeystoneUtils.findEndpointURL (access.getServiceCatalog (), "identity", region, "admin");
387         } catch (RuntimeException e) {
388             String error = "Identity service not found: region=" + region + ",cloud=" + cloudIdentity.getId ();
389             alarmLogger.sendAlarm ("MsoConfigurationError", MsoAlarmLogger.CRITICAL, error);
390             LOGGER.error(MessageEnum.IDENTITY_SERVICE_NOT_FOUND, region, cloudIdentity.getId(), "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in findEndpointURL");
391             throw new MsoAdapterException (error, e);
392         }
393
394         // A new Keystone object is required for the new URL. Use the auth token from above.
395         // Note: this doesn't go back to Openstack, it's just a local object.
396         keystone = new Keystone (adminUrl);
397         keystone.token (token);
398
399         // Cache to avoid re-authentication for every call.
400         KeystoneCacheEntry cacheEntry = new KeystoneCacheEntry (adminUrl, token, access.getToken ().getExpires ());
401         adminClientCache.put (cloudId, cacheEntry);
402
403         return keystone;
404     }
405
406     /*
407      * Find a tenant (or query its existance) by its Id.
408      *
409      * @param adminClient an authenticated Keystone object
410      *
411      * @param tenantName the tenant ID to query
412      *
413      * @return a Tenant object or null if not found
414      */
415     private Tenant findTenantById (Keystone adminClient, String tenantId) {
416         if (tenantId == null) {
417             return null;
418         }
419
420         try {
421             OpenStackRequest <Tenant> request = adminClient.tenants ().show (tenantId);
422             return executeAndRecordOpenstackRequest (request, msoProps);
423         } catch (OpenStackResponseException e) {
424             if (e.getStatus () == 404) {
425                 return null;
426             } else {
427                 LOGGER.error(MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack Error, GET Tenant by Id (" + tenantId + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack GET tenant by Id");
428                 throw e;
429             }
430         }
431     }
432
433     /*
434      * Find a tenant (or query its existance) by its Name. This method avoids an
435      * initial lookup by ID when it's known that we have the tenant Name.
436      *
437      * @param adminClient an authenticated Keystone object
438      *
439      * @param tenantName the tenant name to query
440      *
441      * @return a Tenant object or null if not found
442      */
443     public Tenant findTenantByName (Keystone adminClient, String tenantName) {
444         if (tenantName == null) {
445             return null;
446         }
447
448         try {
449             OpenStackRequest <Tenant> request = adminClient.tenants ().show ("").queryParam ("name", tenantName);
450             return executeAndRecordOpenstackRequest (request, msoProps);
451         } catch (OpenStackResponseException e) {
452             if (e.getStatus () == 404) {
453                 return null;
454             } else {
455                 LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack Error, GET Tenant By Name (" + tenantName + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack GET Tenant By Name");
456                 throw e;
457             }
458         }
459     }
460
461     /*
462      * Look up an Openstack User by Name or Openstack ID. Check the ID first, and if that
463      * fails, try the Name.
464      *
465      * @param adminClient an authenticated Keystone object
466      *
467      * @param userName the user name or ID to query
468      *
469      * @return a User object or null if not found
470      */
471     private User findUserByNameOrId (Keystone adminClient, String userNameOrId) {
472         if (userNameOrId == null) {
473             return null;
474         }
475
476         try {
477             OpenStackRequest <User> request = adminClient.users ().show (userNameOrId);
478             return executeAndRecordOpenstackRequest (request, msoProps);
479         } catch (OpenStackResponseException e) {
480             if (e.getStatus () == 404) {
481                 // Not found by ID. Search for name
482                 return findUserByName (adminClient, userNameOrId);
483             } else {
484                 LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack Error, GET User (" + userNameOrId + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack GET User");
485                 throw e;
486             }
487         }
488     }
489
490     /*
491      * Look up an Openstack User by Name. This avoids initial Openstack query by ID
492      * if we know we have the User Name.
493      *
494      * @param adminClient an authenticated Keystone object
495      *
496      * @param userName the user name to query
497      *
498      * @return a User object or null if not found
499      */
500     public User findUserByName (Keystone adminClient, String userName) {
501         if (userName == null) {
502             return null;
503         }
504
505         try {
506             OpenStackRequest <User> request = adminClient.users ().show ("").queryParam ("name", userName);
507             return executeAndRecordOpenstackRequest (request, msoProps);
508         } catch (OpenStackResponseException e) {
509             if (e.getStatus () == 404) {
510                 return null;
511             } else {
512                 LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack Error, GET User By Name (" + userName + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack GET User By Name");
513                 throw e;
514             }
515         }
516     }
517
518     /*
519      * Look up an Openstack Role by Name or Id. There is no direct query for Roles, so
520      * need to retrieve a full list from Openstack and look for a match. By default,
521      * Openstack should have a "_member_" role for normal VM-level privileges and an
522      * "admin" role for expanded privileges (e.g. administer tenants, users, and roles).
523      * <p>
524      *
525      * @param adminClient an authenticated Keystone object
526      *
527      * @param roleNameOrId the Role name or ID to look up
528      *
529      * @return a Role object
530      */
531     private  Role findRoleByNameOrId (Keystone adminClient, String roleNameOrId) {
532         if (roleNameOrId == null) {
533             return null;
534         }
535
536         // Search by name or ID. Must search in list
537         OpenStackRequest <Roles> request = adminClient.roles ().list ();
538         Roles roles = executeAndRecordOpenstackRequest (request, msoProps);
539
540         for (Role role : roles) {
541             if (roleNameOrId.equals (role.getName ()) || roleNameOrId.equals (role.getId ())) {
542                 return role;
543             }
544         }
545
546         return null;
547     }
548
549     private static class KeystoneCacheEntry implements Serializable {
550
551         private static final long serialVersionUID = 1L;
552
553         private String keystoneUrl;
554         private String token;
555         private Calendar expires;
556
557         public KeystoneCacheEntry (String url, String token, Calendar expires) {
558             this.keystoneUrl = url;
559             this.token = token;
560             this.expires = expires;
561         }
562
563         public Keystone getKeystoneClient () {
564             Keystone keystone = new Keystone (keystoneUrl);
565             keystone.token (token);
566             return keystone;
567         }
568
569         public boolean isExpired () {
570             return expires == null || System.currentTimeMillis() > expires.getTimeInMillis();
571
572         }
573     }
574
575     /**
576      * Clean up the Admin client cache to remove expired entries.
577      */
578     public static void adminCacheCleanup () {
579         for (String cacheKey : adminClientCache.keySet ()) {
580             if (adminClientCache.get (cacheKey).isExpired ()) {
581                 adminClientCache.remove (cacheKey);
582                 LOGGER.debug ("Cleaned Up Cached Admin Client for " + cacheKey);
583             }
584         }
585     }
586
587     /**
588      * Reset the Admin client cache.
589      * This may be useful if cached credentials get out of sync.
590      */
591     public static void adminCacheReset () {
592         adminClientCache = new HashMap<>();
593     }
594
595         @Override
596         public String getKeystoneUrl(String regionId, String msoPropID, CloudIdentity cloudIdentity) {
597                 return cloudIdentity.getIdentityUrl();
598         }
599 }