2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6 * ================================================================================
7 * Modifications Copyright (c) 2019 Samsung
8 * ================================================================================
9 * Modifications Copyright (c) 2020 Nokia
10 * ================================================================================
11 * Licensed under the Apache License, Version 2.0 (the "License");
12 * you may not use this file except in compliance with the License.
13 * You may obtain a copy of the License at
15 * http://www.apache.org/licenses/LICENSE-2.0
17 * Unless required by applicable law or agreed to in writing, software
18 * distributed under the License is distributed on an "AS IS" BASIS,
19 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 * See the License for the specific language governing permissions and
21 * limitations under the License.
22 * ============LICENSE_END=========================================================
25 package org.onap.so.openstack.utils;
28 import java.util.HashMap;
30 import java.util.Optional;
31 import org.onap.so.db.catalog.beans.CloudIdentity;
32 import org.onap.so.db.catalog.beans.CloudSite;
33 import org.onap.logging.filter.base.ErrorCode;
34 import org.onap.so.logger.MessageEnum;
35 import org.onap.so.openstack.beans.MsoTenant;
36 import org.onap.so.openstack.exceptions.MsoAdapterException;
37 import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound;
38 import org.onap.so.openstack.exceptions.MsoException;
39 import org.onap.so.openstack.exceptions.MsoOpenstackException;
40 import org.onap.so.openstack.exceptions.MsoTenantAlreadyExists;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import org.springframework.stereotype.Component;
44 import com.woorea.openstack.base.client.OpenStackBaseException;
45 import com.woorea.openstack.base.client.OpenStackConnectException;
46 import com.woorea.openstack.base.client.OpenStackRequest;
47 import com.woorea.openstack.base.client.OpenStackResponseException;
48 import com.woorea.openstack.keystone.Keystone;
49 import com.woorea.openstack.keystone.model.Access;
50 import com.woorea.openstack.keystone.model.Authentication;
51 import com.woorea.openstack.keystone.model.Metadata;
52 import com.woorea.openstack.keystone.model.Role;
53 import com.woorea.openstack.keystone.model.Roles;
54 import com.woorea.openstack.keystone.model.Tenant;
55 import com.woorea.openstack.keystone.model.User;
56 import com.woorea.openstack.keystone.utils.KeystoneUtils;
59 public class MsoKeystoneUtils extends MsoTenantUtils {
61 public static final String DELETE_TENANT = "Delete Tenant";
62 private static final Logger LOGGER = LoggerFactory.getLogger(MsoKeystoneUtils.class);
65 * Create a tenant with the specified name in the given cloud. If the tenant already exists, an Exception will be
66 * thrown. The MSO User will also be added to the "member" list of the new tenant to perform subsequent Nova/Heat
67 * commands in the tenant. If the MSO User association fails, the entire transaction will be rolled back.
69 * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin requests go to the centralized
70 * identity service in DCP. However, if some artifact must exist in each local LCP instance as well, then it will be
71 * needed to access the correct region.
74 * @param tenantName The tenant name to create
75 * @param cloudSiteId The cloud identifier (may be a region) in which to create the tenant.
76 * @return the tenant ID of the newly created tenant
77 * @throws MsoTenantAlreadyExists Thrown if the requested tenant already exists
78 * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
80 public String createTenant(String tenantName, String cloudSiteId, Map<String, String> metadata, boolean backout)
82 // Obtain the cloud site information where we will create the tenant
83 Optional<CloudSite> cloudSiteOpt = cloudConfig.getCloudSite(cloudSiteId);
84 if (!cloudSiteOpt.isPresent()) {
85 LOGGER.error("{} MSOCloudSite {} not found {} ", MessageEnum.RA_CREATE_TENANT_ERR, cloudSiteId,
86 ErrorCode.DataError.getValue());
87 throw new MsoCloudSiteNotFound(cloudSiteId);
89 Keystone keystoneAdminClient = getKeystoneAdminClient(cloudSiteOpt.get());
92 // Check if the tenant already exists
93 tenant = findTenantByName(keystoneAdminClient, tenantName);
96 // Tenant already exists. Throw an exception
97 LOGGER.error("{} Tenant name {} already exists on Cloud site id {}, {}",
98 MessageEnum.RA_TENANT_ALREADY_EXIST, tenantName, cloudSiteId, ErrorCode.DataError.getValue());
99 throw new MsoTenantAlreadyExists(tenantName, cloudSiteId);
102 // Does not exist, create a new one
103 tenant = new Tenant();
104 tenant.setName(tenantName);
105 tenant.setDescription("SDN Tenant (via MSO)");
106 tenant.setEnabled(true);
108 OpenStackRequest<Tenant> request = keystoneAdminClient.tenants().create(tenant);
109 tenant = executeAndRecordOpenstackRequest(request);
110 } catch (OpenStackBaseException e) {
111 // Convert Keystone OpenStackResponseException to MsoOpenstackException
112 throw keystoneErrorToMsoException(e, "CreateTenant");
113 } catch (RuntimeException e) {
115 throw runtimeExceptionToMsoException(e, "CreateTenant");
118 // Add MSO User to the tenant as a member and
119 // apply tenant metadata if supported by the cloud site
121 CloudIdentity cloudIdentity = cloudSiteOpt.get().getIdentityService();
123 User msoUser = findUserByNameOrId(keystoneAdminClient, cloudIdentity.getMsoId());
124 Role memberRole = findRoleByNameOrId(keystoneAdminClient, cloudIdentity.getMemberRole());
126 if (msoUser != null && memberRole != null) {
127 OpenStackRequest<Void> request =
128 keystoneAdminClient.tenants().addUser(tenant.getId(), msoUser.getId(), memberRole.getId());
129 executeAndRecordOpenstackRequest(request);
132 if (cloudIdentity.getTenantMetadata() && metadata != null && !metadata.isEmpty()) {
133 Metadata tenantMetadata = new Metadata();
134 tenantMetadata.setMetadata(metadata);
136 OpenStackRequest<Metadata> metaRequest =
137 keystoneAdminClient.tenants().createOrUpdateMetadata(tenant.getId(), tenantMetadata);
138 executeAndRecordOpenstackRequest(metaRequest);
140 } catch (Exception e) {
141 // Failed to attach MSO User to the new tenant. Can't operate without access,
142 // so roll back the tenant.
144 LOGGER.warn("{} Create Tenant errored, Tenant deletion suppressed {} ",
145 MessageEnum.RA_CREATE_TENANT_ERR, ErrorCode.DataError.getValue());
148 OpenStackRequest<Void> request = keystoneAdminClient.tenants().delete(tenant.getId());
149 executeAndRecordOpenstackRequest(request);
150 } catch (Exception e2) {
151 // Just log this one. We will report the original exception.
152 LOGGER.error("{} Nested exception rolling back tenant {} ", MessageEnum.RA_CREATE_TENANT_ERR,
153 ErrorCode.DataError.getValue(), e2);
158 // Propagate the original exception on user/role/tenant mapping
159 if (e instanceof OpenStackBaseException) {
160 // Convert Keystone Exception to MsoOpenstackException
161 throw keystoneErrorToMsoException((OpenStackBaseException) e, "CreateTenantUser");
163 MsoAdapterException me = new MsoAdapterException(e.getMessage(), e);
164 me.addContext("CreateTenantUser");
168 return tenant.getId();
172 * Query for a tenant by ID in the given cloud. If the tenant exists, return an MsoTenant object. If not, return
175 * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin requests go to the centralized
176 * identity service in DCP. However, if some artifact must exist in each local LCP instance as well, then it will be
177 * needed to access the correct region.
180 * @param tenantId The Openstack ID of the tenant to query
181 * @param cloudSiteId The cloud identifier (may be a region) in which to query the tenant.
182 * @return the tenant properties of the queried tenant, or null if not found
183 * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
185 public MsoTenant queryTenant(String tenantId, String cloudSiteId) throws MsoException {
186 // Obtain the cloud site information where we will query the tenant
187 CloudSite cloudSite =
188 cloudConfig.getCloudSite(cloudSiteId).orElseThrow(() -> new MsoCloudSiteNotFound(cloudSiteId));
190 Keystone keystoneAdminClient = getKeystoneAdminClient(cloudSite);
192 // Check if the tenant exists and return its Tenant Id
194 Tenant tenant = findTenantById(keystoneAdminClient, tenantId);
195 if (tenant == null) {
199 Map<String, String> metadata = new HashMap<>();
200 if (cloudSite.getIdentityService().getTenantMetadata()) {
201 OpenStackRequest<Metadata> request = keystoneAdminClient.tenants().showMetadata(tenant.getId());
202 Metadata tenantMetadata = executeAndRecordOpenstackRequest(request);
203 if (tenantMetadata != null) {
204 metadata = tenantMetadata.getMetadata();
207 return new MsoTenant(tenant.getId(), tenant.getName(), metadata);
208 } catch (OpenStackBaseException e) {
209 // Convert Keystone OpenStackResponseException to MsoOpenstackException
210 throw keystoneErrorToMsoException(e, "QueryTenant");
211 } catch (RuntimeException e) {
213 throw runtimeExceptionToMsoException(e, "QueryTenant");
218 * Query for a tenant with the specified name in the given cloud. If the tenant exists, return an MsoTenant object.
219 * If not, return null. This query is useful if the client knows it has the tenant name, skipping an initial lookup
220 * by ID that would always fail.
222 * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin requests go to the centralized
223 * identity service in DCP. However, if some artifact must exist in each local LCP instance as well, then it will be
224 * needed to access the correct region.
227 * @param tenantName The name of the tenant to query
228 * @param cloudSiteId The cloud identifier (may be a region) in which to query the tenant.
229 * @return the tenant properties of the queried tenant, or null if not found
230 * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
232 public MsoTenant queryTenantByName(String tenantName, String cloudSiteId) throws MsoException {
233 // Obtain the cloud site information where we will query the tenant
234 CloudSite cloudSite =
235 cloudConfig.getCloudSite(cloudSiteId).orElseThrow(() -> new MsoCloudSiteNotFound(cloudSiteId));
236 Keystone keystoneAdminClient = getKeystoneAdminClient(cloudSite);
239 Tenant tenant = findTenantByName(keystoneAdminClient, tenantName);
240 if (tenant == null) {
244 Map<String, String> metadata = new HashMap<>();
245 if (cloudSite.getIdentityService().getTenantMetadata()) {
246 OpenStackRequest<Metadata> request = keystoneAdminClient.tenants().showMetadata(tenant.getId());
247 Metadata tenantMetadata = executeAndRecordOpenstackRequest(request);
248 if (tenantMetadata != null) {
249 metadata = tenantMetadata.getMetadata();
252 return new MsoTenant(tenant.getId(), tenant.getName(), metadata);
253 } catch (OpenStackBaseException e) {
254 // Convert Keystone OpenStackResponseException to MsoOpenstackException
255 throw keystoneErrorToMsoException(e, "QueryTenantName");
256 } catch (RuntimeException e) {
258 throw runtimeExceptionToMsoException(e, "QueryTenantName");
263 * Delete the specified Tenant (by ID) in the given cloud. This method returns true or false, depending on whether
264 * the tenant existed and was successfully deleted, or if the tenant already did not exist. Both cases are treated
265 * as success (no Exceptions).
267 * Note for the AIC Cloud (DCP/LCP): all admin requests go to the centralized identity service in DCP. So deleting a
268 * tenant from one cloudSiteId will remove it from all sites managed by that identity service.
271 * @param tenantId The Openstack ID of the tenant to delete
272 * @param cloudSiteId The cloud identifier from which to delete the tenant.
273 * @return true if the tenant was deleted, false if the tenant did not exist.
274 * @throws MsoOpenstackException If the Openstack API call returns an exception.
276 public boolean deleteTenant(String tenantId, String cloudSiteId) throws MsoException {
277 // Obtain the cloud site information where we will query the tenant
278 CloudSite cloudSite =
279 cloudConfig.getCloudSite(cloudSiteId).orElseThrow(() -> new MsoCloudSiteNotFound(cloudSiteId));
280 Keystone keystoneAdminClient = getKeystoneAdminClient(cloudSite);
283 // Check that the tenant exists. Also, need the ID to delete
284 Tenant tenant = findTenantById(keystoneAdminClient, tenantId);
285 if (tenant == null) {
286 LOGGER.error("{} Tenant id {} not found on cloud site id {}, {}", MessageEnum.RA_TENANT_NOT_FOUND,
287 tenantId, cloudSiteId, ErrorCode.DataError.getValue());
291 OpenStackRequest<Void> request = keystoneAdminClient.tenants().delete(tenant.getId());
292 executeAndRecordOpenstackRequest(request);
293 LOGGER.debug("Deleted Tenant {} ({})", tenant.getId(), tenant.getName());
294 } catch (OpenStackBaseException e) {
295 // Convert Keystone OpenStackResponseException to MsoOpenstackException
296 throw keystoneErrorToMsoException(e, DELETE_TENANT);
297 } catch (RuntimeException e) {
299 throw runtimeExceptionToMsoException(e, DELETE_TENANT);
306 * Delete the specified Tenant (by Name) in the given cloud. This method returns true or false, depending on whether
307 * the tenant existed and was successfully deleted, or if the tenant already did not exist. Both cases are treated
308 * as success (no Exceptions).
310 * Note for the AIC Cloud (DCP/LCP): all admin requests go to the centralized identity service in DCP. So deleting a
311 * tenant from one cloudSiteId will remove it from all sites managed by that identity service.
314 * @param tenantName The name of the tenant to delete
315 * @param cloudSiteId The cloud identifier from which to delete the tenant.
316 * @return true if the tenant was deleted, false if the tenant did not exist.
317 * @throws MsoOpenstackException If the Openstack API call returns an exception.
319 public boolean deleteTenantByName(String tenantName, String cloudSiteId) throws MsoException {
320 // Obtain the cloud site information where we will query the tenant
321 Optional<CloudSite> cloudSite = cloudConfig.getCloudSite(cloudSiteId);
322 if (!cloudSite.isPresent()) {
323 throw new MsoCloudSiteNotFound(cloudSiteId);
325 Keystone keystoneAdminClient = getKeystoneAdminClient(cloudSite.get());
328 // Need the Tenant ID to delete (can't directly delete by name)
329 Tenant tenant = findTenantByName(keystoneAdminClient, tenantName);
330 if (tenant == null) {
331 // OK if tenant already doesn't exist.
332 LOGGER.error("{} Tenant {} not found on Cloud site id {}, {}", MessageEnum.RA_TENANT_NOT_FOUND,
333 tenantName, cloudSiteId, ErrorCode.DataError.getValue());
337 // Execute the Delete. It has no return value.
338 OpenStackRequest<Void> request = keystoneAdminClient.tenants().delete(tenant.getId());
339 executeAndRecordOpenstackRequest(request);
341 LOGGER.debug("Deleted Tenant {} ({})", tenant.getId(), tenant.getName());
343 } catch (OpenStackBaseException e) {
344 // Note: It doesn't seem to matter if tenant doesn't exist, no exception is thrown.
345 // Convert Keystone OpenStackResponseException to MsoOpenstackException
346 throw keystoneErrorToMsoException(e, DELETE_TENANT);
347 } catch (RuntimeException e) {
349 throw runtimeExceptionToMsoException(e, DELETE_TENANT);
355 // -------------------------------------------------------------------
356 // PRIVATE UTILITY FUNCTIONS FOR USE WITHIN THIS CLASS
359 * Get a Keystone Admin client for the Openstack Identity service. This requires an 'admin'-level userId + password
360 * along with an 'admin' tenant in the target cloud. These values will be retrieved from properties based on the
361 * specified cloud ID. <p> On successful authentication, the Keystone object will be cached for the cloudId so that
362 * it can be reused without going back to Openstack every time.
366 * @return an authenticated Keystone object
368 public Keystone getKeystoneAdminClient(CloudSite cloudSite) throws MsoException {
369 CloudIdentity cloudIdentity = cloudSite.getIdentityService();
371 String adminTenantName = cloudIdentity.getAdminTenant();
372 String region = cloudSite.getRegionId();
374 MsoTenantUtils tenantUtils =
375 tenantUtilsFactory.getTenantUtilsByServerType(cloudIdentity.getIdentityServerType());
376 final String keystoneUrl = tenantUtils.getKeystoneUrl(region, cloudIdentity);
377 Keystone keystone = new Keystone(keystoneUrl);
379 // Must authenticate against the 'admin' tenant to get the services endpoints
380 Access access = null;
383 Authentication credentials = authenticationMethodFactory.getAuthenticationFor(cloudIdentity);
384 OpenStackRequest<Access> request =
385 keystone.tokens().authenticate(credentials).withTenantName(adminTenantName);
386 access = executeAndRecordOpenstackRequest(request);
387 token = access.getToken().getId();
388 } catch (OpenStackResponseException e) {
389 if (e.getStatus() == 401) {
390 // Authentication error. Can't access admin tenant - something is mis-configured
391 String error = "MSO Authentication Failed for " + cloudIdentity.getId();
393 throw new MsoAdapterException(error);
395 throw keystoneErrorToMsoException(e, "TokenAuth");
397 } catch (OpenStackConnectException e) {
398 // Connection to Openstack failed
399 throw keystoneErrorToMsoException(e, "TokenAuth");
402 // Get the Identity service URL. Throws runtime exception if not found per region.
403 String adminUrl = null;
405 adminUrl = KeystoneUtils.findEndpointURL(access.getServiceCatalog(), "identity", region, "public");
406 adminUrl = adminUrl.replaceFirst("5000", "35357");
407 } catch (RuntimeException e) {
408 String error = "Identity service not found: region=" + region + ",cloud=" + cloudIdentity.getId();
410 LOGGER.error("{} Region: {} Cloud identity {} {} Exception in findEndpointURL ",
411 MessageEnum.IDENTITY_SERVICE_NOT_FOUND, region, cloudIdentity.getId(),
412 ErrorCode.DataError.getValue(), e);
413 throw new MsoAdapterException(error, e);
416 // A new Keystone object is required for the new URL. Use the auth token from above.
417 // Note: this doesn't go back to Openstack, it's just a local object.
418 keystone = new Keystone(adminUrl);
419 keystone.token(token);
424 * Find a tenant (or query its existance) by its Name or Id. Check first against the ID. If that fails, then try by
427 * @param adminClient an authenticated Keystone object
429 * @param tenantName the tenant name or ID to query
431 * @return a Tenant object or null if not found
433 public Tenant findTenantByNameOrId(Keystone adminClient, String tenantNameOrId) {
434 if (tenantNameOrId == null) {
438 Tenant tenant = findTenantById(adminClient, tenantNameOrId);
439 if (tenant == null) {
440 tenant = findTenantByName(adminClient, tenantNameOrId);
447 * Find a tenant (or query its existance) by its Id.
449 * @param adminClient an authenticated Keystone object
451 * @param tenantName the tenant ID to query
453 * @return a Tenant object or null if not found
455 private Tenant findTenantById(Keystone adminClient, String tenantId) {
456 if (tenantId == null) {
461 OpenStackRequest<Tenant> request = adminClient.tenants().show(tenantId);
462 return executeAndRecordOpenstackRequest(request);
463 } catch (OpenStackResponseException e) {
464 if (e.getStatus() == 404) {
467 LOGGER.error("{} {} Openstack Error, GET Tenant by Id ({}): ", MessageEnum.RA_CONNECTION_EXCEPTION,
468 ErrorCode.DataError.getValue(), tenantId, e);
475 * Find a tenant (or query its existance) by its Name. This method avoids an initial lookup by ID when it's known
476 * that we have the tenant Name.
478 * @param adminClient an authenticated Keystone object
480 * @param tenantName the tenant name to query
482 * @return a Tenant object or null if not found
484 public Tenant findTenantByName(Keystone adminClient, String tenantName) {
485 if (tenantName == null) {
490 OpenStackRequest<Tenant> request = adminClient.tenants().show("").queryParam("name", tenantName);
491 return executeAndRecordOpenstackRequest(request);
492 } catch (OpenStackResponseException e) {
493 if (e.getStatus() == 404) {
496 LOGGER.error("{} {} Openstack Error, GET Tenant By Name ({}) ", MessageEnum.RA_CONNECTION_EXCEPTION,
497 ErrorCode.DataError.getValue(), tenantName, e);
504 * Look up an Openstack User by Name or Openstack ID. Check the ID first, and if that fails, try the Name.
506 * @param adminClient an authenticated Keystone object
508 * @param userName the user name or ID to query
510 * @return a User object or null if not found
512 private User findUserByNameOrId(Keystone adminClient, String userNameOrId) {
513 if (userNameOrId == null) {
518 OpenStackRequest<User> request = adminClient.users().show(userNameOrId);
519 return executeAndRecordOpenstackRequest(request);
520 } catch (OpenStackResponseException e) {
521 if (e.getStatus() == 404) {
522 // Not found by ID. Search for name
523 return findUserByName(adminClient, userNameOrId);
525 LOGGER.error("{} {} Openstack Error, GET User ({}) ", MessageEnum.RA_CONNECTION_EXCEPTION,
526 ErrorCode.DataError.getValue(), userNameOrId, e);
533 * Look up an Openstack User by Name. This avoids initial Openstack query by ID if we know we have the User Name.
535 * @param adminClient an authenticated Keystone object
537 * @param userName the user name to query
539 * @return a User object or null if not found
541 public User findUserByName(Keystone adminClient, String userName) {
542 if (userName == null) {
547 OpenStackRequest<User> request = adminClient.users().show("").queryParam("name", userName);
548 return executeAndRecordOpenstackRequest(request);
549 } catch (OpenStackResponseException e) {
550 if (e.getStatus() == 404) {
553 LOGGER.error("{} {} Openstack Error, GET User By Name ({}): ", MessageEnum.RA_CONNECTION_EXCEPTION,
554 ErrorCode.DataError.getValue(), userName, e);
561 * Look up an Openstack Role by Name or Id. There is no direct query for Roles, so need to retrieve a full list from
562 * Openstack and look for a match. By default, Openstack should have a "_member_" role for normal VM-level
563 * privileges and an "admin" role for expanded privileges (e.g. administer tenants, users, and roles). <p>
565 * @param adminClient an authenticated Keystone object
567 * @param roleNameOrId the Role name or ID to look up
569 * @return a Role object
571 private Role findRoleByNameOrId(Keystone adminClient, String roleNameOrId) {
572 if (roleNameOrId == null) {
576 // Search by name or ID. Must search in list
577 OpenStackRequest<Roles> request = adminClient.roles().list();
578 Roles roles = executeAndRecordOpenstackRequest(request);
580 for (Role role : roles) {
581 if (roleNameOrId.equals(role.getName()) || roleNameOrId.equals(role.getId())) {
590 public String getKeystoneUrl(String regionId, CloudIdentity cloudIdentity) throws MsoException {
591 return cloudIdentity.getIdentityUrl();