2 * ============LICENSE_START=======================================================
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
11 * http://www.apache.org/licenses/LICENSE-2.0
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=========================================================
21 package org.openecomp.mso.openstack.utils;
24 import java.io.Serializable;
25 import java.util.Calendar;
26 import java.util.HashMap;
29 import java.util.Optional;
31 import org.openecomp.mso.cloud.CloudConfigFactory;
32 import org.openecomp.mso.cloud.CloudIdentity;
33 import org.openecomp.mso.cloud.CloudSite;
34 import org.openecomp.mso.logger.MsoAlarmLogger;
35 import org.openecomp.mso.logger.MsoLogger;
36 import org.openecomp.mso.logger.MessageEnum;
37 import org.openecomp.mso.openstack.beans.MsoTenant;
38 import org.openecomp.mso.openstack.exceptions.MsoAdapterException;
39 import org.openecomp.mso.openstack.exceptions.MsoCloudSiteNotFound;
40 import org.openecomp.mso.openstack.exceptions.MsoException;
41 import org.openecomp.mso.openstack.exceptions.MsoOpenstackException;
42 import org.openecomp.mso.openstack.exceptions.MsoTenantAlreadyExists;
43 import com.woorea.openstack.base.client.OpenStackBaseException;
44 import com.woorea.openstack.base.client.OpenStackConnectException;
45 import com.woorea.openstack.base.client.OpenStackRequest;
46 import com.woorea.openstack.base.client.OpenStackResponseException;
47 import com.woorea.openstack.keystone.Keystone;
48 import com.woorea.openstack.keystone.model.Access;
49 import com.woorea.openstack.keystone.model.Metadata;
50 import com.woorea.openstack.keystone.model.Role;
51 import com.woorea.openstack.keystone.model.Roles;
52 import com.woorea.openstack.keystone.model.Tenant;
53 import com.woorea.openstack.keystone.model.User;
54 import com.woorea.openstack.keystone.utils.KeystoneUtils;
55 import com.woorea.openstack.keystone.model.Authentication;
57 public class MsoKeystoneUtils extends MsoTenantUtils {
59 // Cache the Keystone Clients statically. Since there is just one MSO user, there is no
60 // benefit to re-authentication on every request (or across different flows). The
61 // token will be used until it expires.
63 // The cache key is "cloudId"
64 private static Map <String, KeystoneCacheEntry> adminClientCache = new HashMap<>();
66 private static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA);
69 public MsoKeystoneUtils(String msoPropID, CloudConfigFactory cloudConfigFactory) {
70 super(msoPropID, cloudConfigFactory);
71 this.msoPropID = msoPropID;
72 LOGGER.debug("MsoKeyStoneUtils:" + msoPropID);
76 * Create a tenant with the specified name in the given cloud. If the tenant already exists,
77 * an Exception will be thrown. The MSO User will also be added to the "member" list of
78 * the new tenant to perform subsequent Nova/Heat commands in the tenant. If the MSO User
79 * association fails, the entire transaction will be rolled back.
81 * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin
82 * requests go to the centralized identity service in DCP. However, if some artifact
83 * must exist in each local LCP instance as well, then it will be needed to access the
87 * @param tenantName The tenant name to create
88 * @param cloudSiteId The cloud identifier (may be a region) in which to create the tenant.
89 * @return the tenant ID of the newly created tenant
90 * @throws MsoTenantAlreadyExists Thrown if the requested tenant already exists
91 * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
93 public String createTenant (String tenantName,
95 Map <String, String> metadata,
96 boolean backout) throws MsoException {
97 // Obtain the cloud site information where we will create the tenant
98 Optional<CloudSite> cloudSiteOpt = getCloudConfigFactory().getCloudConfig().getCloudSite(cloudSiteId);
99 if (!cloudSiteOpt.isPresent()) {
100 LOGGER.error(MessageEnum.RA_CREATE_TENANT_ERR, "MSOCloudSite not found", "", "", MsoLogger.ErrorCode.DataError, "MSOCloudSite not found");
101 throw new MsoCloudSiteNotFound (cloudSiteId);
103 Keystone keystoneAdminClient = getKeystoneAdminClient(cloudSiteOpt.get());
104 Tenant tenant = null;
106 // Check if the tenant already exists
107 tenant = findTenantByName (keystoneAdminClient, tenantName);
109 if (tenant != null) {
110 // Tenant already exists. Throw an exception
111 LOGGER.error(MessageEnum.RA_TENANT_ALREADY_EXIST, tenantName, cloudSiteId, "", "", MsoLogger.ErrorCode.DataError, "Tenant already exists");
112 throw new MsoTenantAlreadyExists (tenantName, cloudSiteId);
115 // Does not exist, create a new one
116 tenant = new Tenant ();
117 tenant.setName (tenantName);
118 tenant.setDescription ("SDN Tenant (via MSO)");
119 tenant.setEnabled (true);
121 OpenStackRequest <Tenant> request = keystoneAdminClient.tenants ().create (tenant);
122 tenant = executeAndRecordOpenstackRequest (request, msoProps);
123 } catch (OpenStackBaseException e) {
124 // Convert Keystone OpenStackResponseException to MsoOpenstackException
125 throw keystoneErrorToMsoException (e, "CreateTenant");
126 } catch (RuntimeException e) {
128 throw runtimeExceptionToMsoException (e, "CreateTenant");
131 // Add MSO User to the tenant as a member and
132 // apply tenant metadata if supported by the cloud site
134 CloudIdentity cloudIdentity = cloudSiteOpt.get().getIdentityService ();
136 User msoUser = findUserByNameOrId (keystoneAdminClient, cloudIdentity.getMsoId ());
137 Role memberRole = findRoleByNameOrId (keystoneAdminClient, cloudIdentity.getMemberRole ());
139 OpenStackRequest <Void> request = keystoneAdminClient.tenants ().addUser (tenant.getId (),
141 memberRole.getId ());
142 executeAndRecordOpenstackRequest (request, msoProps);
144 if (cloudIdentity.hasTenantMetadata () && metadata != null && !metadata.isEmpty ()) {
145 Metadata tenantMetadata = new Metadata ();
146 tenantMetadata.setMetadata (metadata);
148 OpenStackRequest <Metadata> metaRequest = keystoneAdminClient.tenants ()
149 .createOrUpdateMetadata (tenant.getId (),
151 executeAndRecordOpenstackRequest (metaRequest, msoProps);
153 } catch (Exception e) {
154 // Failed to attach MSO User to the new tenant. Can't operate without access,
155 // so roll back the tenant.
158 LOGGER.warn(MessageEnum.RA_CREATE_TENANT_ERR, "Create Tenant errored, Tenant deletion suppressed", "Openstack", "", MsoLogger.ErrorCode.DataError, "Create Tenant error, Tenant deletion suppressed");
163 OpenStackRequest <Void> request = keystoneAdminClient.tenants ().delete (tenant.getId ());
164 executeAndRecordOpenstackRequest (request, msoProps);
165 } catch (Exception e2) {
166 // Just log this one. We will report the original exception.
167 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);
172 // Propagate the original exception on user/role/tenant mapping
173 if (e instanceof OpenStackBaseException) {
174 // Convert Keystone Exception to MsoOpenstackException
175 throw keystoneErrorToMsoException ((OpenStackBaseException) e, "CreateTenantUser");
177 MsoAdapterException me = new MsoAdapterException (e.getMessage (), e);
178 me.addContext ("CreateTenantUser");
182 return tenant.getId ();
186 * Query for a tenant by ID in the given cloud. If the tenant exists,
187 * return an MsoTenant object. If not, return null.
189 * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin
190 * requests go to the centralized identity service in DCP. However, if some artifact
191 * must exist in each local LCP instance as well, then it will be needed to access the
195 * @param tenantId The Openstack ID of the tenant to query
196 * @param cloudSiteId The cloud identifier (may be a region) in which to query the tenant.
197 * @return the tenant properties of the queried tenant, or null if not found
198 * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
200 public MsoTenant queryTenant (String tenantId, String cloudSiteId) throws MsoException {
201 // Obtain the cloud site information where we will query the tenant
202 CloudSite cloudSite = getCloudConfigFactory().getCloudConfig().getCloudSite(cloudSiteId).orElseThrow(
203 () -> new MsoCloudSiteNotFound(cloudSiteId));
205 Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite);
207 // Check if the tenant exists and return its Tenant Id
209 Tenant tenant = findTenantById (keystoneAdminClient, tenantId);
210 if (tenant == null) {
214 Map <String, String> metadata = new HashMap<>();
215 if (cloudSite.getIdentityService ().hasTenantMetadata ()) {
216 OpenStackRequest <Metadata> request = keystoneAdminClient.tenants ().showMetadata (tenant.getId ());
217 Metadata tenantMetadata = executeAndRecordOpenstackRequest (request, msoProps);
218 if (tenantMetadata != null) {
219 metadata = tenantMetadata.getMetadata ();
222 return new MsoTenant (tenant.getId (), tenant.getName (), metadata);
223 } catch (OpenStackBaseException e) {
224 // Convert Keystone OpenStackResponseException to MsoOpenstackException
225 throw keystoneErrorToMsoException (e, "QueryTenant");
226 } catch (RuntimeException e) {
228 throw runtimeExceptionToMsoException (e, "QueryTenant");
233 * Query for a tenant with the specified name in the given cloud. If the tenant exists,
234 * return an MsoTenant object. If not, return null. This query is useful if the client
235 * knows it has the tenant name, skipping an initial lookup by ID that would always fail.
237 * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin
238 * requests go to the centralized identity service in DCP. However, if some artifact
239 * must exist in each local LCP instance as well, then it will be needed to access the
243 * @param tenantName The name of the tenant to query
244 * @param cloudSiteId The cloud identifier (may be a region) in which to query the tenant.
245 * @return the tenant properties of the queried tenant, or null if not found
246 * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
248 public MsoTenant queryTenantByName (String tenantName, String cloudSiteId) throws MsoException {
249 // Obtain the cloud site information where we will query the tenant
250 CloudSite cloudSite = getCloudConfigFactory().getCloudConfig().getCloudSite(cloudSiteId).orElseThrow(
251 () -> new MsoCloudSiteNotFound(cloudSiteId));
252 Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite);
255 Tenant tenant = findTenantByName (keystoneAdminClient, tenantName);
256 if (tenant == null) {
260 Map <String, String> metadata = new HashMap<>();
261 if (cloudSite.getIdentityService ().hasTenantMetadata ()) {
262 OpenStackRequest <Metadata> request = keystoneAdminClient.tenants ().showMetadata (tenant.getId ());
263 Metadata tenantMetadata = executeAndRecordOpenstackRequest (request, msoProps);
264 if (tenantMetadata != null) {
265 metadata = tenantMetadata.getMetadata ();
268 return new MsoTenant (tenant.getId (), tenant.getName (), metadata);
269 } catch (OpenStackBaseException e) {
270 // Convert Keystone OpenStackResponseException to MsoOpenstackException
271 throw keystoneErrorToMsoException (e, "QueryTenantName");
272 } catch (RuntimeException e) {
274 throw runtimeExceptionToMsoException (e, "QueryTenantName");
279 * Delete the specified Tenant (by ID) in the given cloud. This method returns true or
280 * false, depending on whether the tenant existed and was successfully deleted, or if
281 * the tenant already did not exist. Both cases are treated as success (no Exceptions).
283 * Note for the AIC Cloud (DCP/LCP): all admin requests go to the centralized identity
284 * service in DCP. So deleting a tenant from one cloudSiteId will remove it from all
285 * sites managed by that identity service.
288 * @param tenantId The Openstack ID of the tenant to delete
289 * @param cloudSiteId The cloud identifier from which to delete the tenant.
290 * @return true if the tenant was deleted, false if the tenant did not exist.
291 * @throws MsoOpenstackException If the Openstack API call returns an exception.
293 public boolean deleteTenant (String tenantId, String cloudSiteId) throws MsoException {
294 // Obtain the cloud site information where we will query the tenant
295 CloudSite cloudSite = getCloudConfigFactory().getCloudConfig().getCloudSite(cloudSiteId).orElseThrow(
296 () -> new MsoCloudSiteNotFound(cloudSiteId));
297 Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite);
300 // Check that the tenant exists. Also, need the ID to delete
301 Tenant tenant = findTenantById (keystoneAdminClient, tenantId);
302 if (tenant == null) {
303 LOGGER.error(MessageEnum.RA_TENANT_NOT_FOUND, tenantId, cloudSiteId, "", "", MsoLogger.ErrorCode.DataError, "Tenant not found");
307 OpenStackRequest <Void> request = keystoneAdminClient.tenants ().delete (tenant.getId ());
308 executeAndRecordOpenstackRequest (request, msoProps);
309 LOGGER.debug ("Deleted Tenant " + tenant.getId () + " (" + tenant.getName () + ")");
311 // Clear any cached clients. Not really needed, ID will not be reused.
312 MsoHeatUtils.expireHeatClient (tenant.getId (), cloudSiteId);
313 MsoNeutronUtils.expireNeutronClient (tenant.getId (), cloudSiteId);
314 } catch (OpenStackBaseException e) {
315 // Convert Keystone OpenStackResponseException to MsoOpenstackException
316 throw keystoneErrorToMsoException (e, "Delete Tenant");
317 } catch (RuntimeException e) {
319 throw runtimeExceptionToMsoException (e, "DeleteTenant");
325 // -------------------------------------------------------------------
326 // PRIVATE UTILITY FUNCTIONS FOR USE WITHIN THIS CLASS
329 * Get a Keystone Admin client for the Openstack Identity service.
330 * This requires an 'admin'-level userId + password along with an 'admin' tenant
331 * in the target cloud. These values will be retrieved from properties based
332 * on the specified cloud ID.
334 * On successful authentication, the Keystone object will be cached for the cloudId
335 * so that it can be reused without going back to Openstack every time.
339 * @return an authenticated Keystone object
341 public Keystone getKeystoneAdminClient (CloudSite cloudSite) throws MsoException {
342 CloudIdentity cloudIdentity = cloudSite.getIdentityService ();
344 String cloudId = cloudIdentity.getId ();
345 String adminTenantName = cloudIdentity.getAdminTenant ();
346 String region = cloudSite.getRegionId ();
348 // Check first in the cache of previously authorized clients
349 KeystoneCacheEntry entry = adminClientCache.get (cloudId);
351 if (!entry.isExpired ()) {
352 return entry.getKeystoneClient ();
354 // Token is expired. Remove it from cache.
355 adminClientCache.remove (cloudId);
359 Keystone keystone = new Keystone (cloudIdentity.getKeystoneUrl (region, msoPropID));
361 // Must authenticate against the 'admin' tenant to get the services endpoints
362 Access access = null;
365 Authentication credentials = cloudIdentity.getAuthentication ();
366 OpenStackRequest <Access> request = keystone.tokens ()
367 .authenticate (credentials)
368 .withTenantName (adminTenantName);
369 access = executeAndRecordOpenstackRequest (request, msoProps);
370 token = access.getToken ().getId ();
371 } catch (OpenStackResponseException e) {
372 if (e.getStatus () == 401) {
373 // Authentication error. Can't access admin tenant - something is mis-configured
374 String error = "MSO Authentication Failed for " + cloudIdentity.getId ();
375 alarmLogger.sendAlarm ("MsoAuthenticationError", MsoAlarmLogger.CRITICAL, error);
376 throw new MsoAdapterException (error);
378 throw keystoneErrorToMsoException (e, "TokenAuth");
380 } catch (OpenStackConnectException e) {
381 // Connection to Openstack failed
382 throw keystoneErrorToMsoException (e, "TokenAuth");
385 // Get the Identity service URL. Throws runtime exception if not found per region.
386 String adminUrl = null;
388 // TODO: FOR TESTING!!!!
389 adminUrl = KeystoneUtils.findEndpointURL (access.getServiceCatalog (), "identity", region, "public");
390 adminUrl = adminUrl.replaceFirst("5000", "35357");
391 } catch (RuntimeException e) {
392 String error = "Identity service not found: region=" + region + ",cloud=" + cloudIdentity.getId ();
393 alarmLogger.sendAlarm ("MsoConfigurationError", MsoAlarmLogger.CRITICAL, error);
394 LOGGER.error(MessageEnum.IDENTITY_SERVICE_NOT_FOUND, region, cloudIdentity.getId(), "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in findEndpointURL");
395 throw new MsoAdapterException (error, e);
398 // A new Keystone object is required for the new URL. Use the auth token from above.
399 // Note: this doesn't go back to Openstack, it's just a local object.
400 keystone = new Keystone (adminUrl);
401 keystone.token (token);
403 // Cache to avoid re-authentication for every call.
404 KeystoneCacheEntry cacheEntry = new KeystoneCacheEntry (adminUrl, token, access.getToken ().getExpires ());
405 adminClientCache.put (cloudId, cacheEntry);
411 * Find a tenant (or query its existance) by its Id.
413 * @param adminClient an authenticated Keystone object
415 * @param tenantName the tenant ID to query
417 * @return a Tenant object or null if not found
419 private Tenant findTenantById (Keystone adminClient, String tenantId) {
420 if (tenantId == null) {
425 OpenStackRequest <Tenant> request = adminClient.tenants ().show (tenantId);
426 return executeAndRecordOpenstackRequest (request, msoProps);
427 } catch (OpenStackResponseException e) {
428 if (e.getStatus () == 404) {
431 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");
438 * Find a tenant (or query its existance) by its Name. This method avoids an
439 * initial lookup by ID when it's known that we have the tenant Name.
441 * @param adminClient an authenticated Keystone object
443 * @param tenantName the tenant name to query
445 * @return a Tenant object or null if not found
447 public Tenant findTenantByName (Keystone adminClient, String tenantName) {
448 if (tenantName == null) {
453 OpenStackRequest <Tenant> request = adminClient.tenants ().show ("").queryParam ("name", tenantName);
454 return executeAndRecordOpenstackRequest (request, msoProps);
455 } catch (OpenStackResponseException e) {
456 if (e.getStatus () == 404) {
459 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");
466 * Look up an Openstack User by Name or Openstack ID. Check the ID first, and if that
467 * fails, try the Name.
469 * @param adminClient an authenticated Keystone object
471 * @param userName the user name or ID to query
473 * @return a User object or null if not found
475 private User findUserByNameOrId (Keystone adminClient, String userNameOrId) {
476 if (userNameOrId == null) {
481 OpenStackRequest <User> request = adminClient.users ().show (userNameOrId);
482 return executeAndRecordOpenstackRequest (request, msoProps);
483 } catch (OpenStackResponseException e) {
484 if (e.getStatus () == 404) {
485 // Not found by ID. Search for name
486 return findUserByName (adminClient, userNameOrId);
488 LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack Error, GET User (" + userNameOrId + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack GET User");
495 * Look up an Openstack User by Name. This avoids initial Openstack query by ID
496 * if we know we have the User Name.
498 * @param adminClient an authenticated Keystone object
500 * @param userName the user name to query
502 * @return a User object or null if not found
504 public User findUserByName (Keystone adminClient, String userName) {
505 if (userName == null) {
510 OpenStackRequest <User> request = adminClient.users ().show ("").queryParam ("name", userName);
511 return executeAndRecordOpenstackRequest (request, msoProps);
512 } catch (OpenStackResponseException e) {
513 if (e.getStatus () == 404) {
516 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");
523 * Look up an Openstack Role by Name or Id. There is no direct query for Roles, so
524 * need to retrieve a full list from Openstack and look for a match. By default,
525 * Openstack should have a "_member_" role for normal VM-level privileges and an
526 * "admin" role for expanded privileges (e.g. administer tenants, users, and roles).
529 * @param adminClient an authenticated Keystone object
531 * @param roleNameOrId the Role name or ID to look up
533 * @return a Role object
535 private Role findRoleByNameOrId (Keystone adminClient, String roleNameOrId) {
536 if (roleNameOrId == null) {
540 // Search by name or ID. Must search in list
541 OpenStackRequest <Roles> request = adminClient.roles ().list ();
542 Roles roles = executeAndRecordOpenstackRequest (request, msoProps);
544 for (Role role : roles) {
545 if (roleNameOrId.equals (role.getName ()) || roleNameOrId.equals (role.getId ())) {
553 private static class KeystoneCacheEntry implements Serializable {
555 private static final long serialVersionUID = 1L;
557 private String keystoneUrl;
558 private String token;
559 private Calendar expires;
561 public KeystoneCacheEntry (String url, String token, Calendar expires) {
562 this.keystoneUrl = url;
564 this.expires = expires;
567 public Keystone getKeystoneClient () {
568 Keystone keystone = new Keystone (keystoneUrl);
569 keystone.token (token);
573 public boolean isExpired () {
574 // adding arbitrary guard timer of 5 minutes
575 return expires == null || System.currentTimeMillis() > (expires.getTimeInMillis() - 300000);
581 * Clean up the Admin client cache to remove expired entries.
583 public static void adminCacheCleanup () {
584 for (String cacheKey : adminClientCache.keySet ()) {
585 if (adminClientCache.get (cacheKey).isExpired ()) {
586 adminClientCache.remove (cacheKey);
587 LOGGER.debug ("Cleaned Up Cached Admin Client for " + cacheKey);
593 * Reset the Admin client cache.
594 * This may be useful if cached credentials get out of sync.
596 public static void adminCacheReset () {
597 adminClientCache = new HashMap<>();
601 public String getKeystoneUrl(String regionId, String msoPropID, CloudIdentity cloudIdentity) {
602 return cloudIdentity.getIdentityUrl();