2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6 * ================================================================================
7 * Modifications Copyright (c) 2019 Samsung
8 * ================================================================================
9 * Licensed under the Apache License, Version 2.0 (the "License");
10 * you may not use this file except in compliance with the License.
11 * You may obtain a copy of the License at
13 * http://www.apache.org/licenses/LICENSE-2.0
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
20 * ============LICENSE_END=========================================================
23 package org.onap.so.openstack.utils;
26 import com.woorea.openstack.base.client.OpenStackBaseException;
27 import com.woorea.openstack.base.client.OpenStackConnectException;
28 import com.woorea.openstack.base.client.OpenStackRequest;
29 import com.woorea.openstack.base.client.OpenStackResponseException;
30 import com.woorea.openstack.keystone.Keystone;
31 import com.woorea.openstack.keystone.model.Access;
32 import com.woorea.openstack.keystone.model.Authentication;
33 import com.woorea.openstack.keystone.model.Metadata;
34 import com.woorea.openstack.keystone.model.Role;
35 import com.woorea.openstack.keystone.model.Roles;
36 import com.woorea.openstack.keystone.model.Tenant;
37 import com.woorea.openstack.keystone.model.User;
38 import com.woorea.openstack.keystone.utils.KeystoneUtils;
39 import java.util.HashMap;
41 import java.util.Optional;
42 import org.onap.so.cloud.authentication.AuthenticationMethodFactory;
43 import org.onap.so.db.catalog.beans.CloudIdentity;
44 import org.onap.so.db.catalog.beans.CloudSite;
45 import org.onap.so.logger.ErrorCode;
46 import org.onap.so.logger.MessageEnum;
47 import org.onap.so.openstack.beans.MsoTenant;
48 import org.onap.so.openstack.exceptions.MsoAdapterException;
49 import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound;
50 import org.onap.so.openstack.exceptions.MsoException;
51 import org.onap.so.openstack.exceptions.MsoOpenstackException;
52 import org.onap.so.openstack.exceptions.MsoTenantAlreadyExists;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55 import org.springframework.beans.factory.annotation.Autowired;
56 import org.springframework.stereotype.Component;
59 public class MsoKeystoneUtils extends MsoTenantUtils {
61 private static Logger logger = LoggerFactory.getLogger(MsoKeystoneUtils.class);
64 private AuthenticationMethodFactory authenticationMethodFactory;
67 private MsoHeatUtils msoHeatUtils;
70 private MsoNeutronUtils msoNeutronUtils;
73 private MsoTenantUtilsFactory tenantUtilsFactory;
75 * Create a tenant with the specified name in the given cloud. If the tenant already exists,
76 * an Exception will be thrown. The MSO User will also be added to the "member" list of
77 * the new tenant to perform subsequent Nova/Heat commands in the tenant. If the MSO User
78 * association fails, the entire transaction will be rolled back.
80 * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin
81 * requests go to the centralized identity service in DCP. However, if some artifact
82 * must exist in each local LCP instance as well, then it will be needed to access the
86 * @param tenantName The tenant name to create
87 * @param cloudSiteId The cloud identifier (may be a region) in which to create the tenant.
88 * @return the tenant ID of the newly created tenant
89 * @throws MsoTenantAlreadyExists Thrown if the requested tenant already exists
90 * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
92 public String createTenant (String tenantName,
94 Map <String, String> metadata,
95 boolean backout) throws MsoException {
96 // Obtain the cloud site information where we will create the tenant
97 Optional<CloudSite> cloudSiteOpt = cloudConfig.getCloudSite(cloudSiteId);
98 if (!cloudSiteOpt.isPresent()) {
99 logger.error("{} MSOCloudSite {} not found {} ", MessageEnum.RA_CREATE_TENANT_ERR, cloudSiteId,
100 ErrorCode.DataError.getValue());
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("{} Tenant name {} already exists on Cloud site id {}, {}",
112 MessageEnum.RA_TENANT_ALREADY_EXIST, tenantName, cloudSiteId, ErrorCode.DataError.getValue());
113 throw new MsoTenantAlreadyExists (tenantName, cloudSiteId);
116 // Does not exist, create a new one
117 tenant = new Tenant ();
118 tenant.setName (tenantName);
119 tenant.setDescription ("SDN Tenant (via MSO)");
120 tenant.setEnabled (true);
122 OpenStackRequest <Tenant> request = keystoneAdminClient.tenants ().create (tenant);
123 tenant = executeAndRecordOpenstackRequest (request);
124 } catch (OpenStackBaseException e) {
125 // Convert Keystone OpenStackResponseException to MsoOpenstackException
126 throw keystoneErrorToMsoException (e, "CreateTenant");
127 } catch (RuntimeException e) {
129 throw runtimeExceptionToMsoException (e, "CreateTenant");
132 // Add MSO User to the tenant as a member and
133 // apply tenant metadata if supported by the cloud site
135 CloudIdentity cloudIdentity = cloudSiteOpt.get().getIdentityService();
137 User msoUser = findUserByNameOrId (keystoneAdminClient, cloudIdentity.getMsoId ());
138 Role memberRole = findRoleByNameOrId (keystoneAdminClient, cloudIdentity.getMemberRole ());
140 if(msoUser != null && memberRole != null) {
141 OpenStackRequest <Void> request = keystoneAdminClient.tenants ().addUser (tenant.getId (),
143 memberRole.getId ());
144 executeAndRecordOpenstackRequest (request);
147 if (cloudIdentity.getTenantMetadata () && metadata != null && !metadata.isEmpty ()) {
148 Metadata tenantMetadata = new Metadata ();
149 tenantMetadata.setMetadata (metadata);
151 OpenStackRequest <Metadata> metaRequest = keystoneAdminClient.tenants ()
152 .createOrUpdateMetadata (tenant.getId (),
154 executeAndRecordOpenstackRequest (metaRequest);
156 } catch (Exception e) {
157 // Failed to attach MSO User to the new tenant. Can't operate without access,
158 // so roll back the tenant.
161 logger.warn("{} Create Tenant errored, Tenant deletion suppressed {} ", MessageEnum.RA_CREATE_TENANT_ERR,
162 ErrorCode.DataError.getValue());
167 OpenStackRequest <Void> request = keystoneAdminClient.tenants ().delete (tenant.getId ());
168 executeAndRecordOpenstackRequest (request);
169 } catch (Exception e2) {
170 // Just log this one. We will report the original exception.
171 logger.error("{} Nested exception rolling back tenant {} ", MessageEnum.RA_CREATE_TENANT_ERR,
172 ErrorCode.DataError.getValue(), e2);
177 // Propagate the original exception on user/role/tenant mapping
178 if (e instanceof OpenStackBaseException) {
179 // Convert Keystone Exception to MsoOpenstackException
180 throw keystoneErrorToMsoException ((OpenStackBaseException) e, "CreateTenantUser");
182 MsoAdapterException me = new MsoAdapterException (e.getMessage (), e);
183 me.addContext ("CreateTenantUser");
187 return tenant.getId ();
191 * Query for a tenant by ID in the given cloud. If the tenant exists,
192 * return an MsoTenant object. If not, return null.
194 * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin
195 * requests go to the centralized identity service in DCP. However, if some artifact
196 * must exist in each local LCP instance as well, then it will be needed to access the
200 * @param tenantId The Openstack ID of the tenant to query
201 * @param cloudSiteId The cloud identifier (may be a region) in which to query the tenant.
202 * @return the tenant properties of the queried tenant, or null if not found
203 * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
205 public MsoTenant queryTenant (String tenantId, String cloudSiteId) throws MsoException {
206 // Obtain the cloud site information where we will query the tenant
207 CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
208 () -> new MsoCloudSiteNotFound(cloudSiteId));
210 Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite);
212 // Check if the tenant exists and return its Tenant Id
214 Tenant tenant = findTenantById (keystoneAdminClient, tenantId);
215 if (tenant == null) {
219 Map <String, String> metadata = new HashMap <String, String> ();
220 if (cloudSite.getIdentityService().getTenantMetadata ()) {
221 OpenStackRequest <Metadata> request = keystoneAdminClient.tenants ().showMetadata (tenant.getId ());
222 Metadata tenantMetadata = executeAndRecordOpenstackRequest (request);
223 if (tenantMetadata != null) {
224 metadata = tenantMetadata.getMetadata ();
227 return new MsoTenant (tenant.getId (), tenant.getName (), metadata);
228 } catch (OpenStackBaseException e) {
229 // Convert Keystone OpenStackResponseException to MsoOpenstackException
230 throw keystoneErrorToMsoException (e, "QueryTenant");
231 } catch (RuntimeException e) {
233 throw runtimeExceptionToMsoException (e, "QueryTenant");
238 * Query for a tenant with the specified name in the given cloud. If the tenant exists,
239 * return an MsoTenant object. If not, return null. This query is useful if the client
240 * knows it has the tenant name, skipping an initial lookup by ID that would always fail.
242 * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin
243 * requests go to the centralized identity service in DCP. However, if some artifact
244 * must exist in each local LCP instance as well, then it will be needed to access the
248 * @param tenantName The name of the tenant to query
249 * @param cloudSiteId The cloud identifier (may be a region) in which to query the tenant.
250 * @return the tenant properties of the queried tenant, or null if not found
251 * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
253 public MsoTenant queryTenantByName (String tenantName, String cloudSiteId) throws MsoException {
254 // Obtain the cloud site information where we will query the tenant
255 CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
256 () -> new MsoCloudSiteNotFound(cloudSiteId));
257 Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite);
260 Tenant tenant = findTenantByName (keystoneAdminClient, tenantName);
261 if (tenant == null) {
265 Map <String, String> metadata = new HashMap <String, String> ();
266 if (cloudSite.getIdentityService().getTenantMetadata ()) {
267 OpenStackRequest <Metadata> request = keystoneAdminClient.tenants ().showMetadata (tenant.getId ());
268 Metadata tenantMetadata = executeAndRecordOpenstackRequest (request);
269 if (tenantMetadata != null) {
270 metadata = tenantMetadata.getMetadata ();
273 return new MsoTenant (tenant.getId (), tenant.getName (), metadata);
274 } catch (OpenStackBaseException e) {
275 // Convert Keystone OpenStackResponseException to MsoOpenstackException
276 throw keystoneErrorToMsoException (e, "QueryTenantName");
277 } catch (RuntimeException e) {
279 throw runtimeExceptionToMsoException (e, "QueryTenantName");
284 * Delete the specified Tenant (by ID) in the given cloud. This method returns true or
285 * false, depending on whether the tenant existed and was successfully deleted, or if
286 * the tenant already did not exist. Both cases are treated as success (no Exceptions).
288 * Note for the AIC Cloud (DCP/LCP): all admin requests go to the centralized identity
289 * service in DCP. So deleting a tenant from one cloudSiteId will remove it from all
290 * sites managed by that identity service.
293 * @param tenantId The Openstack ID of the tenant to delete
294 * @param cloudSiteId The cloud identifier from which to delete the tenant.
295 * @return true if the tenant was deleted, false if the tenant did not exist.
296 * @throws MsoOpenstackException If the Openstack API call returns an exception.
298 public boolean deleteTenant (String tenantId, String cloudSiteId) throws MsoException {
299 // Obtain the cloud site information where we will query the tenant
300 CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
301 () -> new MsoCloudSiteNotFound(cloudSiteId));
302 Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite);
305 // Check that the tenant exists. Also, need the ID to delete
306 Tenant tenant = findTenantById (keystoneAdminClient, tenantId);
307 if (tenant == null) {
308 logger.error("{} Tenant id {} not found on cloud site id {}, {}", MessageEnum.RA_TENANT_NOT_FOUND,
309 tenantId, cloudSiteId, ErrorCode.DataError.getValue());
313 OpenStackRequest <Void> request = keystoneAdminClient.tenants ().delete (tenant.getId ());
314 executeAndRecordOpenstackRequest (request);
315 logger.debug ("Deleted Tenant {} ({})", tenant.getId(), tenant.getName());
316 } catch (OpenStackBaseException e) {
317 // Convert Keystone OpenStackResponseException to MsoOpenstackException
318 throw keystoneErrorToMsoException (e, "Delete Tenant");
319 } catch (RuntimeException e) {
321 throw runtimeExceptionToMsoException (e, "DeleteTenant");
328 * Delete the specified Tenant (by Name) in the given cloud. This method returns true or
329 * false, depending on whether the tenant existed and was successfully deleted, or if
330 * the tenant already did not exist. Both cases are treated as success (no Exceptions).
332 * Note for the AIC Cloud (DCP/LCP): all admin requests go to the centralized identity
333 * service in DCP. So deleting a tenant from one cloudSiteId will remove it from all
334 * sites managed by that identity service.
337 * @param tenantName The name of the tenant to delete
338 * @param cloudSiteId The cloud identifier from which to delete the tenant.
339 * @return true if the tenant was deleted, false if the tenant did not exist.
340 * @throws MsoOpenstackException If the Openstack API call returns an exception.
342 public boolean deleteTenantByName (String tenantName, String cloudSiteId) throws MsoException {
343 // Obtain the cloud site information where we will query the tenant
344 Optional<CloudSite> cloudSite = cloudConfig.getCloudSite (cloudSiteId);
345 if (!cloudSite.isPresent()) {
346 throw new MsoCloudSiteNotFound (cloudSiteId);
348 Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite.get());
351 // Need the Tenant ID to delete (can't directly delete by name)
352 Tenant tenant = findTenantByName (keystoneAdminClient, tenantName);
353 if (tenant == null) {
354 // OK if tenant already doesn't exist.
355 logger.error("{} Tenant {} not found on Cloud site id {}, {}", MessageEnum.RA_TENANT_NOT_FOUND,
356 tenantName, cloudSiteId, ErrorCode.DataError.getValue());
360 // Execute the Delete. It has no return value.
361 OpenStackRequest <Void> request = keystoneAdminClient.tenants ().delete (tenant.getId ());
362 executeAndRecordOpenstackRequest (request);
364 logger.debug("Deleted Tenant {} ({})", tenant.getId(), tenant.getName());
366 } catch (OpenStackBaseException e) {
367 // Note: It doesn't seem to matter if tenant doesn't exist, no exception is thrown.
368 // Convert Keystone OpenStackResponseException to MsoOpenstackException
369 throw keystoneErrorToMsoException (e, "DeleteTenant");
370 } catch (RuntimeException e) {
372 throw runtimeExceptionToMsoException (e, "DeleteTenant");
378 // -------------------------------------------------------------------
379 // PRIVATE UTILITY FUNCTIONS FOR USE WITHIN THIS CLASS
382 * Get a Keystone Admin client for the Openstack Identity service.
383 * This requires an 'admin'-level userId + password along with an 'admin' tenant
384 * in the target cloud. These values will be retrieved from properties based
385 * on the specified cloud ID.
387 * On successful authentication, the Keystone object will be cached for the cloudId
388 * so that it can be reused without going back to Openstack every time.
392 * @return an authenticated Keystone object
394 public Keystone getKeystoneAdminClient (CloudSite cloudSite) throws MsoException {
395 CloudIdentity cloudIdentity = cloudSite.getIdentityService();
397 String cloudId = cloudIdentity.getId ();
398 String adminTenantName = cloudIdentity.getAdminTenant ();
399 String region = cloudSite.getRegionId ();
401 MsoTenantUtils tenantUtils = tenantUtilsFactory.getTenantUtilsByServerType(cloudIdentity.getIdentityServerType());
402 final String keystoneUrl = tenantUtils.getKeystoneUrl(region, cloudIdentity);
403 Keystone keystone = new Keystone(keystoneUrl);
405 // Must authenticate against the 'admin' tenant to get the services endpoints
406 Access access = null;
409 Authentication credentials = authenticationMethodFactory.getAuthenticationFor(cloudIdentity);
410 OpenStackRequest <Access> request = keystone.tokens ()
411 .authenticate (credentials)
412 .withTenantName (adminTenantName);
413 access = executeAndRecordOpenstackRequest (request);
414 token = access.getToken ().getId ();
415 } catch (OpenStackResponseException e) {
416 if (e.getStatus () == 401) {
417 // Authentication error. Can't access admin tenant - something is mis-configured
418 String error = "MSO Authentication Failed for " + cloudIdentity.getId ();
420 throw new MsoAdapterException (error);
422 throw keystoneErrorToMsoException (e, "TokenAuth");
424 } catch (OpenStackConnectException e) {
425 // Connection to Openstack failed
426 throw keystoneErrorToMsoException (e, "TokenAuth");
429 // Get the Identity service URL. Throws runtime exception if not found per region.
430 String adminUrl = null;
432 // TODO: FOR TESTING!!!!
433 adminUrl = KeystoneUtils.findEndpointURL (access.getServiceCatalog (), "identity", region, "public");
434 adminUrl = adminUrl.replaceFirst("5000", "35357");
435 } catch (RuntimeException e) {
436 String error = "Identity service not found: region=" + region + ",cloud=" + cloudIdentity.getId ();
438 logger.error("{} Region: {} Cloud identity {} {} Exception in findEndpointURL ",
439 MessageEnum.IDENTITY_SERVICE_NOT_FOUND, region, cloudIdentity.getId(),
440 ErrorCode.DataError.getValue(), e);
441 throw new MsoAdapterException (error, e);
444 // A new Keystone object is required for the new URL. Use the auth token from above.
445 // Note: this doesn't go back to Openstack, it's just a local object.
446 keystone = new Keystone (adminUrl);
447 keystone.token (token);
452 * Find a tenant (or query its existance) by its Name or Id. Check first against the
453 * ID. If that fails, then try by name.
455 * @param adminClient an authenticated Keystone object
457 * @param tenantName the tenant name or ID to query
459 * @return a Tenant object or null if not found
461 public Tenant findTenantByNameOrId (Keystone adminClient, String tenantNameOrId) {
462 if (tenantNameOrId == null) {
466 Tenant tenant = findTenantById (adminClient, tenantNameOrId);
467 if (tenant == null) {
468 tenant = findTenantByName (adminClient, tenantNameOrId);
475 * Find a tenant (or query its existance) by its Id.
477 * @param adminClient an authenticated Keystone object
479 * @param tenantName the tenant ID to query
481 * @return a Tenant object or null if not found
483 private Tenant findTenantById (Keystone adminClient, String tenantId) {
484 if (tenantId == null) {
489 OpenStackRequest <Tenant> request = adminClient.tenants ().show (tenantId);
490 return executeAndRecordOpenstackRequest (request);
491 } catch (OpenStackResponseException e) {
492 if (e.getStatus () == 404) {
495 logger.error("{} {} Openstack Error, GET Tenant by Id ({}): ", MessageEnum.RA_CONNECTION_EXCEPTION,
496 ErrorCode.DataError.getValue(), tenantId, e);
503 * Find a tenant (or query its existance) by its Name. This method avoids an
504 * initial lookup by ID when it's known that we have the tenant Name.
506 * @param adminClient an authenticated Keystone object
508 * @param tenantName the tenant name to query
510 * @return a Tenant object or null if not found
512 public Tenant findTenantByName (Keystone adminClient, String tenantName) {
513 if (tenantName == null) {
518 OpenStackRequest <Tenant> request = adminClient.tenants ().show ("").queryParam ("name", tenantName);
519 return executeAndRecordOpenstackRequest (request);
520 } catch (OpenStackResponseException e) {
521 if (e.getStatus () == 404) {
524 logger.error("{} {} Openstack Error, GET Tenant By Name ({}) ", MessageEnum.RA_CONNECTION_EXCEPTION,
525 ErrorCode.DataError.getValue(), tenantName, e);
532 * Look up an Openstack User by Name or Openstack ID. Check the ID first, and if that
533 * fails, try the Name.
535 * @param adminClient an authenticated Keystone object
537 * @param userName the user name or ID to query
539 * @return a User object or null if not found
541 private User findUserByNameOrId (Keystone adminClient, String userNameOrId) {
542 if (userNameOrId == null) {
547 OpenStackRequest <User> request = adminClient.users ().show (userNameOrId);
548 return executeAndRecordOpenstackRequest (request);
549 } catch (OpenStackResponseException e) {
550 if (e.getStatus () == 404) {
551 // Not found by ID. Search for name
552 return findUserByName (adminClient, userNameOrId);
554 logger.error("{} {} Openstack Error, GET User ({}) ", MessageEnum.RA_CONNECTION_EXCEPTION,
555 ErrorCode.DataError.getValue(), userNameOrId, e);
562 * Look up an Openstack User by Name. This avoids initial Openstack query by ID
563 * if we know we have the User Name.
565 * @param adminClient an authenticated Keystone object
567 * @param userName the user name to query
569 * @return a User object or null if not found
571 public User findUserByName (Keystone adminClient, String userName) {
572 if (userName == null) {
577 OpenStackRequest <User> request = adminClient.users ().show ("").queryParam ("name", userName);
578 return executeAndRecordOpenstackRequest (request);
579 } catch (OpenStackResponseException e) {
580 if (e.getStatus () == 404) {
583 logger.error("{} {} Openstack Error, GET User By Name ({}): ", MessageEnum.RA_CONNECTION_EXCEPTION,
584 ErrorCode.DataError.getValue(), userName, e);
591 * Look up an Openstack Role by Name or Id. There is no direct query for Roles, so
592 * need to retrieve a full list from Openstack and look for a match. By default,
593 * Openstack should have a "_member_" role for normal VM-level privileges and an
594 * "admin" role for expanded privileges (e.g. administer tenants, users, and roles).
597 * @param adminClient an authenticated Keystone object
599 * @param roleNameOrId the Role name or ID to look up
601 * @return a Role object
603 private Role findRoleByNameOrId (Keystone adminClient, String roleNameOrId) {
604 if (roleNameOrId == null) {
608 // Search by name or ID. Must search in list
609 OpenStackRequest <Roles> request = adminClient.roles ().list ();
610 Roles roles = executeAndRecordOpenstackRequest (request);
612 for (Role role : roles) {
613 if (roleNameOrId.equals (role.getName ()) || roleNameOrId.equals (role.getId ())) {
622 public String getKeystoneUrl(String regionId, CloudIdentity cloudIdentity) throws MsoException {
623 return cloudIdentity.getIdentityUrl();