From 12dcf5cc6f4727f6193d289b2d656001b26da643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Jandre?= <48719461+JoaoJandre@users.noreply.github.com> Date: Sun, 10 Dec 2023 03:48:15 -0300 Subject: [PATCH] Move subdomains between domains (#7446) Sometimes users have the need to move resources between domains, for example, in a big company, a department may be moved from one part of the company to another, changing the company's department hierarchy, the easiest way of reflecting this change on the company's cloud environment would be to move subdomains between domains, but currently ACS offers no option to do that. This PR adds the moveDomain API, which will move domains between subdomains. Furthermore, if the domain that is being moved has any subdomains, those will also be moved, maintaining the current subdomain tree. --- .../main/java/com/cloud/event/EventTypes.java | 2 + .../java/com/cloud/user/DomainService.java | 3 + .../command/admin/domain/MoveDomainCmd.java | 73 +++++++ .../cloud/network/dao/NetworkDomainDao.java | 3 + .../network/dao/NetworkDomainDaoImpl.java | 56 ++++- .../dao/AffinityGroupDomainMapDao.java | 3 + .../dao/AffinityGroupDomainMapDaoImpl.java | 57 +++++ .../api/query/dao/NetworkOfferingJoinDao.java | 3 + .../query/dao/NetworkOfferingJoinDaoImpl.java | 53 +++++ .../api/query/dao/ServiceOfferingJoinDao.java | 3 + .../query/dao/ServiceOfferingJoinDaoImpl.java | 52 +++++ .../cloud/dc/dao/DedicatedResourceDao.java | 3 + .../dc/dao/DedicatedResourceDaoImpl.java | 53 +++++ .../cloud/server/ManagementServerImpl.java | 2 + .../com/cloud/user/DomainManagerImpl.java | 202 +++++++++++++++++- 15 files changed, 566 insertions(+), 2 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/domain/MoveDomainCmd.java diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 5fce169ffed..67fed5500ee 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -320,6 +320,7 @@ public class EventTypes { public static final String EVENT_DOMAIN_CREATE = "DOMAIN.CREATE"; public static final String EVENT_DOMAIN_DELETE = "DOMAIN.DELETE"; public static final String EVENT_DOMAIN_UPDATE = "DOMAIN.UPDATE"; + public static final String EVENT_DOMAIN_MOVE = "DOMAIN.MOVE"; // Snapshots public static final String EVENT_SNAPSHOT_COPY = "SNAPSHOT.COPY"; @@ -878,6 +879,7 @@ public class EventTypes { entityEventDetails.put(EVENT_DOMAIN_CREATE, Domain.class); entityEventDetails.put(EVENT_DOMAIN_DELETE, Domain.class); entityEventDetails.put(EVENT_DOMAIN_UPDATE, Domain.class); + entityEventDetails.put(EVENT_DOMAIN_MOVE, Domain.class); // Snapshots entityEventDetails.put(EVENT_SNAPSHOT_CREATE, Snapshot.class); diff --git a/api/src/main/java/com/cloud/user/DomainService.java b/api/src/main/java/com/cloud/user/DomainService.java index 3ccfcbcea4c..06109cf5ff7 100644 --- a/api/src/main/java/com/cloud/user/DomainService.java +++ b/api/src/main/java/com/cloud/user/DomainService.java @@ -20,9 +20,11 @@ import java.util.List; import org.apache.cloudstack.api.command.admin.domain.ListDomainChildrenCmd; import org.apache.cloudstack.api.command.admin.domain.ListDomainsCmd; +import org.apache.cloudstack.api.command.admin.domain.MoveDomainCmd; import com.cloud.domain.Domain; import com.cloud.exception.PermissionDeniedException; +import com.cloud.exception.ResourceAllocationException; import com.cloud.utils.Pair; public interface DomainService { @@ -66,4 +68,5 @@ public interface DomainService { */ Domain findDomainByIdOrPath(Long id, String domainPath); + Domain moveDomainAndChildrenToNewParentDomain(MoveDomainCmd cmd) throws ResourceAllocationException; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/MoveDomainCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/MoveDomainCmd.java new file mode 100644 index 00000000000..586345b2de7 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/MoveDomainCmd.java @@ -0,0 +1,73 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.domain; + +import com.cloud.domain.Domain; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DomainResponse; + +@APICommand(name = "moveDomain", description = "Moves a domain and its children to a new parent domain.", since = "4.19.0.0", responseObject = DomainResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin}) +public class MoveDomainCmd extends BaseCmd { + + private static final String APINAME = "moveDomain"; + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "The ID of the domain to be moved.") + private Long domainId; + + @Parameter(name = ApiConstants.PARENT_DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, + description = "The ID of the new parent domain of the domain to be moved.") + private Long parentDomainId; + + public Long getDomainId() { + return domainId; + } + + public Long getParentDomainId() { + return parentDomainId; + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public void execute() throws ResourceAllocationException { + Domain domain = _domainService.moveDomainAndChildrenToNewParentDomain(this); + + if (domain != null) { + DomainResponse response = _responseGenerator.createDomainResponse(domain); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to move the domain."); + } + } +} diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDomainDao.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDomainDao.java index f4d0ad71b8c..43d4d25305e 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDomainDao.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDomainDao.java @@ -17,6 +17,7 @@ package com.cloud.network.dao; import java.util.List; +import java.util.Map; import com.cloud.utils.db.GenericDao; @@ -26,4 +27,6 @@ public interface NetworkDomainDao extends GenericDao { NetworkDomainVO getDomainNetworkMapByNetworkId(long networkId); List listNetworkIdsByDomain(long domainId); + + Map> listDomainsOfSharedNetworksUsedByDomainPath(String domainPath); } diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDomainDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDomainDaoImpl.java index 188f306cecc..ce86a8636a1 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkDomainDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkDomainDaoImpl.java @@ -16,10 +16,17 @@ // under the License. package com.cloud.network.dao; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; - +import com.cloud.utils.db.TransactionLegacy; +import org.apache.log4j.Logger; import org.springframework.stereotype.Component; import com.cloud.utils.db.DB; @@ -31,9 +38,23 @@ import com.cloud.utils.db.SearchCriteria.Op; @Component @DB() public class NetworkDomainDaoImpl extends GenericDaoBase implements NetworkDomainDao { + public static Logger logger = Logger.getLogger(NetworkDomainDaoImpl.class.getName()); final SearchBuilder AllFieldsSearch; final SearchBuilder DomainsSearch; + private static final String LIST_DOMAINS_OF_SHARED_NETWORKS_USED_BY_DOMAIN_PATH = "SELECT shared_nw.domain_id, \n" + + "GROUP_CONCAT('VM:', vm.uuid, ' | NW:' , network.uuid) \n" + + "FROM cloud.domain_network_ref AS shared_nw\n" + + "INNER JOIN cloud.nics AS nic ON (nic.network_id = shared_nw.network_id AND nic.removed IS NULL)\n" + + "INNER JOIN cloud.vm_instance AS vm ON (vm.id = nic.instance_id)\n" + + "INNER JOIN cloud.domain AS domain ON (domain.id = vm.domain_id)\n" + + "INNER JOIN cloud.domain AS domain_sn ON (domain_sn.id = shared_nw.domain_id)\n" + + "INNER JOIN cloud.networks AS network ON (shared_nw.network_id = network.id)\n" + + "WHERE shared_nw.subdomain_access = 1\n" + + "AND domain.path LIKE ?\n" + + "AND domain_sn.path NOT LIKE ?\n" + + "GROUP BY shared_nw.network_id"; + protected NetworkDomainDaoImpl() { super(); @@ -71,4 +92,37 @@ public class NetworkDomainDaoImpl extends GenericDaoBase } return networkIdsToReturn; } + + @Override + public Map> listDomainsOfSharedNetworksUsedByDomainPath(String domainPath) { + logger.debug(String.format("Retrieving the domains of the shared networks with subdomain access used by domain with path [%s].", domainPath)); + + TransactionLegacy txn = TransactionLegacy.currentTxn(); + try (PreparedStatement pstmt = txn.prepareStatement(LIST_DOMAINS_OF_SHARED_NETWORKS_USED_BY_DOMAIN_PATH)) { + Map> domainsOfSharedNetworksUsedByDomainPath = new HashMap<>(); + + String domainSearch = domainPath.concat("%"); + pstmt.setString(1, domainSearch); + pstmt.setString(2, domainSearch); + + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + Long domainId = rs.getLong(1); + List vmUuidsAndNetworkUuids = Arrays.asList(rs.getString(2).split(",")); + + domainsOfSharedNetworksUsedByDomainPath.put(domainId, vmUuidsAndNetworkUuids); + } + } + + return domainsOfSharedNetworksUsedByDomainPath; + } catch (SQLException e) { + logger.error(String.format("Failed to retrieve the domains of the shared networks with subdomain access used by domain with path [%s] due to [%s]. Returning an empty " + + "list of domains.", domainPath, e.getMessage())); + + logger.debug(String.format("Failed to retrieve the domains of the shared networks with subdomain access used by domain with path [%s]. Returning an empty " + + "list of domains.", domainPath), e); + + return new HashMap<>(); + } + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/affinity/dao/AffinityGroupDomainMapDao.java b/engine/schema/src/main/java/org/apache/cloudstack/affinity/dao/AffinityGroupDomainMapDao.java index 07be976f202..27040c2f54e 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/affinity/dao/AffinityGroupDomainMapDao.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/affinity/dao/AffinityGroupDomainMapDao.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.affinity.dao; import java.util.List; +import java.util.Map; import org.apache.cloudstack.affinity.AffinityGroupDomainMapVO; @@ -28,4 +29,6 @@ public interface AffinityGroupDomainMapDao extends GenericDao listByDomain(Object... domainId); + Map> listDomainsOfAffinityGroupsUsedByDomainPath(String domainPath); + } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/affinity/dao/AffinityGroupDomainMapDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/affinity/dao/AffinityGroupDomainMapDaoImpl.java index 5ecf63d6a6c..1dd22df46d7 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/affinity/dao/AffinityGroupDomainMapDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/affinity/dao/AffinityGroupDomainMapDaoImpl.java @@ -16,23 +16,46 @@ // under the License. package org.apache.cloudstack.affinity.dao; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.annotation.PostConstruct; import org.apache.cloudstack.affinity.AffinityGroupDomainMapVO; +import org.apache.log4j.Logger; +import com.cloud.network.dao.NetworkDomainDaoImpl; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Op; +import com.cloud.utils.db.TransactionLegacy; public class AffinityGroupDomainMapDaoImpl extends GenericDaoBase implements AffinityGroupDomainMapDao { + public static Logger logger = Logger.getLogger(NetworkDomainDaoImpl.class.getName()); private SearchBuilder ListByAffinityGroup; private SearchBuilder DomainsSearch; + private static final String LIST_DOMAINS_WITH_AFFINITY_GROUPS_WITH_SUBDOMAIN_ACCESS_USED_BY_DOMAIN_PATH = "SELECT affinity_group_domain_map.domain_id, \n" + + "GROUP_CONCAT('VM:', vm.uuid, ' | AG:' , affinity_group.uuid) \n" + + "FROM cloud.affinity_group_domain_map AS affinity_group_domain_map\n" + + "INNER JOIN cloud.affinity_group_vm_map AS affinity_group_vm_map ON (cloud.affinity_group_domain_map.affinity_group_id = affinity_group_vm_map.affinity_group_id)\n" + + "INNER JOIN cloud.vm_instance AS vm ON (vm.id = affinity_group_vm_map.instance_id)\n" + + "INNER JOIN cloud.domain AS domain ON (domain.id = vm.domain_id)\n" + + "INNER JOIN cloud.domain AS domain_sn ON (domain_sn.id = affinity_group_domain_map.domain_id)\n" + + "INNER JOIN cloud.affinity_group AS affinity_group ON (affinity_group.id = affinity_group_domain_map.affinity_group_id)\n" + + "WHERE affinity_group_domain_map.subdomain_access = 1\n" + + "AND domain.path LIKE ?\n" + + "AND domain_sn.path NOT LIKE ?\n" + + "GROUP BY affinity_group.id"; + public AffinityGroupDomainMapDaoImpl() { } @@ -62,4 +85,38 @@ public class AffinityGroupDomainMapDaoImpl extends GenericDaoBase> listDomainsOfAffinityGroupsUsedByDomainPath(String domainPath) { + logger.debug(String.format("Retrieving the domains of the affinity groups with subdomain access used by domain with path [%s].", domainPath)); + + TransactionLegacy txn = TransactionLegacy.currentTxn(); + try (PreparedStatement pstmt = txn.prepareStatement(LIST_DOMAINS_WITH_AFFINITY_GROUPS_WITH_SUBDOMAIN_ACCESS_USED_BY_DOMAIN_PATH)) { + Map> domainsOfAffinityGroupsUsedByDomainPath = new HashMap<>(); + + String domainSearch = domainPath.concat("%"); + pstmt.setString(1, domainSearch); + pstmt.setString(2, domainSearch); + + + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + Long domainId = rs.getLong(1); + List vmUuidsAndAffinityGroupUuids = Arrays.asList(rs.getString(2).split(",")); + + domainsOfAffinityGroupsUsedByDomainPath.put(domainId, vmUuidsAndAffinityGroupUuids); + } + } + + return domainsOfAffinityGroupsUsedByDomainPath; + } catch (SQLException e) { + logger.error(String.format("Failed to retrieve the domains of the affinity groups with subdomain access used by domain with path [%s] due to [%s]. Returning an " + + "empty list of domains.", domainPath, e.getMessage())); + + logger.debug(String.format("Failed to retrieve the domains of the affinity groups with subdomain access used by domain with path [%s]. Returning an empty " + + "list of domains.", domainPath), e); + + return new HashMap<>(); + } + } + } diff --git a/server/src/main/java/com/cloud/api/query/dao/NetworkOfferingJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/NetworkOfferingJoinDao.java index 767b9acf5d4..f5c58a2ec24 100644 --- a/server/src/main/java/com/cloud/api/query/dao/NetworkOfferingJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/NetworkOfferingJoinDao.java @@ -18,6 +18,7 @@ package com.cloud.api.query.dao; import java.util.List; +import java.util.Map; import org.apache.cloudstack.api.response.NetworkOfferingResponse; @@ -52,4 +53,6 @@ public interface NetworkOfferingJoinDao extends GenericDao> listDomainsOfNetworkOfferingsUsedByDomainPath(String domainPath); } diff --git a/server/src/main/java/com/cloud/api/query/dao/NetworkOfferingJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/NetworkOfferingJoinDaoImpl.java index 474409a976c..d50f1610e69 100644 --- a/server/src/main/java/com/cloud/api/query/dao/NetworkOfferingJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/NetworkOfferingJoinDaoImpl.java @@ -17,8 +17,15 @@ package com.cloud.api.query.dao; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import com.cloud.utils.db.TransactionLegacy; import org.apache.commons.lang3.StringUtils; import org.apache.cloudstack.api.response.NetworkOfferingResponse; import org.apache.log4j.Logger; @@ -44,6 +51,17 @@ public class NetworkOfferingJoinDaoImpl extends GenericDaoBase findByDomainId(long domainId, Boolean includeAllDomainOffering) { SearchBuilder sb = createSearchBuilder(); @@ -120,4 +138,39 @@ public class NetworkOfferingJoinDaoImpl extends GenericDaoBase> listDomainsOfNetworkOfferingsUsedByDomainPath(String domainPath) { + s_logger.debug(String.format("Retrieving the domains of the network offerings used by domain with path [%s].", domainPath)); + + TransactionLegacy txn = TransactionLegacy.currentTxn(); + try (PreparedStatement pstmt = txn.prepareStatement(LIST_DOMAINS_OF_NETWORK_OFFERINGS_USED_BY_DOMAIN_PATH)) { + Map> domainsOfNetworkOfferingsUsedByDomainPath = new HashMap<>(); + + String domainSearch = domainPath.concat("%"); + pstmt.setString(1, domainSearch); + pstmt.setString(2, domainSearch); + + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + Long domainId = rs.getLong(1); + List networkUuids = Arrays.asList(rs.getString(2).split(",")); + + domainsOfNetworkOfferingsUsedByDomainPath.put(domainId, networkUuids); + } + } + + return domainsOfNetworkOfferingsUsedByDomainPath; + } catch (SQLException e) { + s_logger.error(String.format("Failed to retrieve the domains of the network offerings used by domain with path [%s] due to [%s]. Returning an empty " + + "list of domains.", domainPath, e.getMessage())); + + s_logger.debug(String.format("Failed to retrieve the domains of the network offerings used by domain with path [%s]. Returning an empty " + + "list of domains.", domainPath), e); + + return new HashMap<>(); + } + } } diff --git a/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDao.java index 94cc943a812..973e4017529 100644 --- a/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDao.java @@ -17,6 +17,7 @@ package com.cloud.api.query.dao; import java.util.List; +import java.util.Map; import org.apache.cloudstack.api.response.ServiceOfferingResponse; @@ -31,4 +32,6 @@ public interface ServiceOfferingJoinDao extends GenericDao> listDomainsOfServiceOfferingsUsedByDomainPath(String domainPath); } diff --git a/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java index 1cad19e40a2..4a81cc8bfee 100644 --- a/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java @@ -16,12 +16,18 @@ // under the License. package com.cloud.api.query.dao; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import com.cloud.dc.VsphereStoragePolicyVO; import com.cloud.dc.dao.VsphereStoragePolicyDao; import com.cloud.user.AccountManager; +import com.cloud.utils.db.TransactionLegacy; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import com.cloud.storage.DiskOfferingVO; @@ -61,6 +67,18 @@ public class ServiceOfferingJoinDaoImpl extends GenericDaoBase> listDomainsOfServiceOfferingsUsedByDomainPath(String domainPath) { + s_logger.debug(String.format("Retrieving the domains of the service offerings used by domain with path [%s].", domainPath)); + + TransactionLegacy txn = TransactionLegacy.currentTxn(); + try (PreparedStatement pstmt = txn.prepareStatement(LIST_DOMAINS_OF_SERVICE_OFFERINGS_USED_BY_DOMAIN_PATH)) { + Map> domainsOfServiceOfferingsUsedByDomainPath = new HashMap<>(); + + String domainSearch = domainPath.concat("%"); + pstmt.setString(1, domainSearch); + pstmt.setString(2, domainSearch); + + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + Long domainId = rs.getLong(1); + List vmUuids = Arrays.asList(rs.getString(2).split(",")); + + domainsOfServiceOfferingsUsedByDomainPath.put(domainId, vmUuids); + } + } + + return domainsOfServiceOfferingsUsedByDomainPath; + } catch (SQLException e) { + s_logger.error(String.format("Failed to retrieve the domains of the service offerings used by domain with path [%s] due to [%s]. Returning an empty " + + "list of domains.", domainPath, e.getMessage())); + + s_logger.debug(String.format("Failed to retrieve the domains of the service offerings used by domain with path [%s]. Returning an empty " + + "list of domains.", domainPath), e); + + return new HashMap<>(); + } + } } diff --git a/server/src/main/java/com/cloud/dc/dao/DedicatedResourceDao.java b/server/src/main/java/com/cloud/dc/dao/DedicatedResourceDao.java index 1a67ff4feed..c6ed450e2e1 100644 --- a/server/src/main/java/com/cloud/dc/dao/DedicatedResourceDao.java +++ b/server/src/main/java/com/cloud/dc/dao/DedicatedResourceDao.java @@ -17,6 +17,7 @@ package com.cloud.dc.dao; import java.util.List; +import java.util.Map; import com.cloud.dc.DedicatedResourceVO; import com.cloud.utils.Pair; @@ -58,4 +59,6 @@ public interface DedicatedResourceDao extends GenericDao findHostsByCluster(Long clusterId); List findHostsByZone(Long zoneId); + + Map> listDomainsOfDedicatedResourcesUsedByDomainPath(String domainPath); } diff --git a/server/src/main/java/com/cloud/dc/dao/DedicatedResourceDaoImpl.java b/server/src/main/java/com/cloud/dc/dao/DedicatedResourceDaoImpl.java index c10ef2dc25f..31f2e361cd5 100644 --- a/server/src/main/java/com/cloud/dc/dao/DedicatedResourceDaoImpl.java +++ b/server/src/main/java/com/cloud/dc/dao/DedicatedResourceDaoImpl.java @@ -16,7 +16,12 @@ // under the License. package com.cloud.dc.dao; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -26,6 +31,7 @@ import com.cloud.host.HostVO; import com.cloud.host.dao.HostDao; import com.cloud.utils.db.Filter; import org.springframework.stereotype.Component; +import org.apache.log4j.Logger; import com.cloud.dc.DedicatedResourceVO; import com.cloud.utils.Pair; @@ -45,6 +51,8 @@ import javax.inject.Inject; @DB public class DedicatedResourceDaoImpl extends GenericDaoBase implements DedicatedResourceDao { + public static Logger LOGGER = Logger.getLogger(DedicatedResourceDaoImpl.class.getName()); + @Inject protected HostDao hostDao; @@ -79,6 +87,17 @@ public class DedicatedResourceDaoImpl extends GenericDaoBase ListHostsByCluster; protected SearchBuilder ListHostsByZone; + private static final String LIST_DOMAINS_OF_DEDICATED_RESOURCES_USED_BY_DOMAIN_PATH = "SELECT dr.domain_id, \n" + + " GROUP_CONCAT('VM:', vm.uuid) \n" + + " FROM cloud.dedicated_resources AS dr\n" + + " INNER JOIN cloud.vm_instance AS vm ON (vm.domain_id = dr.domain_id)\n" + + " INNER JOIN cloud.domain AS domain ON (domain.id = vm.domain_id) \n" + + " INNER JOIN cloud.domain AS domain_dr ON (domain.id = dr.domain_id) \n" + + " WHERE domain.path LIKE ? \n" + + " AND domain_dr.path NOT LIKE ? \n " + + " AND vm.removed IS NULL \n" + + " GROUP BY dr.id"; + protected DedicatedResourceDaoImpl() { PodSearch = createSearchBuilder(); PodSearch.and("podId", PodSearch.entity().getPodId(), SearchCriteria.Op.EQ); @@ -428,4 +447,38 @@ public class DedicatedResourceDaoImpl extends GenericDaoBase> listDomainsOfDedicatedResourcesUsedByDomainPath(String domainPath) { + LOGGER.debug(String.format("Retrieving the domains of the dedicated resources used by domain with path [%s].", domainPath)); + + TransactionLegacy txn = TransactionLegacy.currentTxn(); + try (PreparedStatement pstmt = txn.prepareStatement(LIST_DOMAINS_OF_DEDICATED_RESOURCES_USED_BY_DOMAIN_PATH)) { + Map> domainsOfDedicatedResourcesUsedByDomainPath = new HashMap<>(); + + String domainSearch = domainPath.concat("%"); + pstmt.setString(1, domainSearch); + pstmt.setString(2, domainSearch); + + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + Long domainId = rs.getLong(1); + List vmUuids = Arrays.asList(rs.getString(2).split(",")); + + domainsOfDedicatedResourcesUsedByDomainPath.put(domainId, vmUuids); + } + } + + return domainsOfDedicatedResourcesUsedByDomainPath; + } catch (SQLException e) { + LOGGER.error(String.format("Failed to retrieve the domains of the dedicated resources used by domain with path [%s] due to [%s]. Returning an empty " + + "list of domains.", domainPath, e.getMessage())); + + LOGGER.debug(String.format("Failed to retrieve the domains of the dedicated resources used by domain with path [%s]. Returning an empty " + + "list of domains.", domainPath), e); + + return new HashMap<>(); + } + } } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 9e88b6f1a1a..f794736a4d5 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -88,6 +88,7 @@ import org.apache.cloudstack.api.command.admin.domain.DeleteDomainCmd; import org.apache.cloudstack.api.command.admin.domain.ListDomainChildrenCmd; import org.apache.cloudstack.api.command.admin.domain.ListDomainsCmd; import org.apache.cloudstack.api.command.admin.domain.ListDomainsCmdByAdmin; +import org.apache.cloudstack.api.command.admin.domain.MoveDomainCmd; import org.apache.cloudstack.api.command.admin.domain.UpdateDomainCmd; import org.apache.cloudstack.api.command.admin.guest.AddGuestOsCmd; import org.apache.cloudstack.api.command.admin.guest.AddGuestOsMappingCmd; @@ -3406,6 +3407,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe cmdList.add(ListDomainsCmd.class); cmdList.add(ListDomainsCmdByAdmin.class); cmdList.add(UpdateDomainCmd.class); + cmdList.add(MoveDomainCmd.class); cmdList.add(AddHostCmd.class); cmdList.add(AddSecondaryStorageCmd.class); cmdList.add(CancelMaintenanceCmd.class); diff --git a/server/src/main/java/com/cloud/user/DomainManagerImpl.java b/server/src/main/java/com/cloud/user/DomainManagerImpl.java index 2dd356aca8e..1551309890c 100644 --- a/server/src/main/java/com/cloud/user/DomainManagerImpl.java +++ b/server/src/main/java/com/cloud/user/DomainManagerImpl.java @@ -17,10 +17,14 @@ package com.cloud.user; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.inject.Inject; @@ -28,16 +32,20 @@ import com.cloud.api.query.dao.NetworkOfferingJoinDao; import com.cloud.api.query.dao.VpcOfferingJoinDao; import com.cloud.api.query.vo.NetworkOfferingJoinVO; import com.cloud.api.query.vo.VpcOfferingJoinVO; +import com.cloud.configuration.Resource; import com.cloud.domain.dao.DomainDetailsDao; import com.cloud.network.vpc.dao.VpcOfferingDao; import com.cloud.network.vpc.dao.VpcOfferingDetailsDao; import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.offerings.dao.NetworkOfferingDetailsDao; +import com.cloud.exception.ResourceAllocationException; +import org.apache.cloudstack.affinity.dao.AffinityGroupDomainMapDao; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.domain.ListDomainChildrenCmd; import org.apache.cloudstack.api.command.admin.domain.ListDomainsCmd; +import org.apache.cloudstack.api.command.admin.domain.MoveDomainCmd; import org.apache.cloudstack.api.command.admin.domain.UpdateDomainCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; @@ -151,6 +159,10 @@ public class DomainManagerImpl extends ManagerBase implements DomainManager, Dom private DomainDetailsDao _domainDetailsDao; @Inject private AnnotationDao annotationDao; + @Inject + private ResourceLimitService resourceLimitService; + @Inject + private AffinityGroupDomainMapDao affinityGroupDomainMapDao; @Inject MessageBus _messageBus; @@ -272,7 +284,7 @@ public class DomainManagerImpl extends ManagerBase implements DomainManager, Dom List domains = _domainDao.search(sc, null); if (!domains.isEmpty()) { - throw new InvalidParameterValueException("Domain with name " + name + " already exists for the parent id=" + parentId); + throw new InvalidParameterValueException(String.format("Domain with name [%s] already exists for the parent with ID [%s].", name, parentId)); } } @@ -910,4 +922,192 @@ public class DomainManagerImpl extends ManagerBase implements DomainManager, Dom } } + @Override + @ActionEvent(eventType = EventTypes.EVENT_DOMAIN_MOVE, eventDescription = "moving Domain") + @DB + public Domain moveDomainAndChildrenToNewParentDomain(MoveDomainCmd cmd) throws ResourceAllocationException { + Long idOfDomainToBeMoved = cmd.getDomainId(); + Long idOfNewParentDomain = cmd.getParentDomainId(); + + if (idOfDomainToBeMoved == Domain.ROOT_DOMAIN) { + throw new InvalidParameterValueException("The domain to be moved cannot be the ROOT domain."); + } + + if (idOfDomainToBeMoved.equals(idOfNewParentDomain)) { + throw new InvalidParameterValueException("The domain to be moved and the new parent domain cannot be the same."); + } + + DomainVO domainToBeMoved = returnDomainIfExistsAndIsActive(idOfDomainToBeMoved); + s_logger.debug(String.format("Found the domain [%s] as the domain to be moved.", domainToBeMoved)); + + DomainVO newParentDomain = returnDomainIfExistsAndIsActive(idOfNewParentDomain); + s_logger.debug(String.format("Found the domain [%s] as the new parent domain of the domain to be moved [%s].", newParentDomain, domainToBeMoved)); + + Account caller = getCaller(); + _accountMgr.checkAccess(caller, domainToBeMoved); + _accountMgr.checkAccess(caller, newParentDomain); + + Long idOfCurrentParentOfDomainToBeMoved = domainToBeMoved.getParent(); + if (idOfCurrentParentOfDomainToBeMoved.equals(idOfNewParentDomain)) { + throw new InvalidParameterValueException(String.format("The current parent domain of the domain to be moved is equal to the new parent domain [%s].", newParentDomain)); + } + + if (newParentDomain.getPath().startsWith(domainToBeMoved.getPath())) { + throw new InvalidParameterValueException("The new parent domain of the domain cannot be one of its children."); + } + + validateUniqueDomainName(domainToBeMoved.getName(), idOfNewParentDomain); + + validateNewParentDomainResourceLimits(domainToBeMoved, newParentDomain); + + String currentPathOfDomainToBeMoved = domainToBeMoved.getPath(); + String domainToBeMovedName = domainToBeMoved.getName().concat("/"); + String newPathOfDomainToBeMoved = newParentDomain.getPath().concat(domainToBeMovedName); + + validateNewParentDomainCanAccessAllDomainToBeMovedResources(domainToBeMoved, newParentDomain, currentPathOfDomainToBeMoved, newPathOfDomainToBeMoved); + + DomainVO parentOfDomainToBeMoved = _domainDao.findById(idOfCurrentParentOfDomainToBeMoved); + Transaction.execute(new TransactionCallbackNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) { + s_logger.debug(String.format("Setting the new parent of the domain to be moved [%s] as [%s].", domainToBeMoved, newParentDomain)); + domainToBeMoved.setParent(idOfNewParentDomain); + + updateDomainAndChildrenPathAndLevel(domainToBeMoved, newParentDomain, currentPathOfDomainToBeMoved, newPathOfDomainToBeMoved); + + updateResourceCounts(idOfCurrentParentOfDomainToBeMoved, idOfNewParentDomain); + + updateChildCounts(parentOfDomainToBeMoved, newParentDomain); + } + }); + + return domainToBeMoved; + } + + protected void validateNewParentDomainResourceLimits(DomainVO domainToBeMoved, DomainVO newParentDomain) throws ResourceAllocationException { + long domainToBeMovedId = domainToBeMoved.getId(); + long newParentDomainId = newParentDomain.getId(); + for (Resource.ResourceType resourceType : Resource.ResourceType.values()) { + long currentDomainResourceCount = _resourceCountDao.getResourceCount(domainToBeMovedId, ResourceOwnerType.Domain, resourceType); + long newParentDomainResourceCount = _resourceCountDao.getResourceCount(newParentDomainId, ResourceOwnerType.Domain, resourceType); + long newParentDomainResourceLimit = resourceLimitService.findCorrectResourceLimitForDomain(newParentDomain, resourceType); + + if (newParentDomainResourceLimit == Resource.RESOURCE_UNLIMITED) { + return; + } + + if (currentDomainResourceCount + newParentDomainResourceCount > newParentDomainResourceLimit) { + String message = String.format("Cannot move domain [%s] to parent domain [%s] as maximum domain resource limit of type [%s] would be exceeded. The current resource " + + "count for domain [%s] is [%s], the resource count for the new parent domain [%s] is [%s], and the limit is [%s].", domainToBeMoved.getUuid(), + newParentDomain.getUuid(), resourceType, domainToBeMoved.getUuid(), currentDomainResourceCount, newParentDomain.getUuid(), newParentDomainResourceCount, + newParentDomainResourceLimit); + s_logger.error(message); + throw new ResourceAllocationException(message, resourceType); + } + } + } + + protected void validateNewParentDomainCanAccessAllDomainToBeMovedResources(DomainVO domainToBeMoved, DomainVO newParentDomain, String currentPathOfDomainToBeMoved, + String newPathOfDomainToBeMoved) { + Map> idsOfDomainsWithNetworksUsedByDomainToBeMoved = _networkDomainDao.listDomainsOfSharedNetworksUsedByDomainPath(currentPathOfDomainToBeMoved); + validateNewParentDomainCanAccessResourcesOfDomainToBeMoved(newPathOfDomainToBeMoved, domainToBeMoved, newParentDomain, idsOfDomainsWithNetworksUsedByDomainToBeMoved, "Networks"); + + Map> idsOfDomainsOfAffinityGroupsUsedByDomainToBeMoved = + affinityGroupDomainMapDao.listDomainsOfAffinityGroupsUsedByDomainPath(currentPathOfDomainToBeMoved); + validateNewParentDomainCanAccessResourcesOfDomainToBeMoved(newPathOfDomainToBeMoved, domainToBeMoved, newParentDomain, idsOfDomainsOfAffinityGroupsUsedByDomainToBeMoved, "Affinity groups"); + + Map> idsOfDomainsOfServiceOfferingsUsedByDomainToBeMoved = + serviceOfferingJoinDao.listDomainsOfServiceOfferingsUsedByDomainPath(currentPathOfDomainToBeMoved); + validateNewParentDomainCanAccessResourcesOfDomainToBeMoved(newPathOfDomainToBeMoved, domainToBeMoved, newParentDomain, idsOfDomainsOfServiceOfferingsUsedByDomainToBeMoved, "Service offerings"); + + Map> idsOfDomainsOfNetworkOfferingsUsedByDomainToBeMoved = + networkOfferingJoinDao.listDomainsOfNetworkOfferingsUsedByDomainPath(currentPathOfDomainToBeMoved); + validateNewParentDomainCanAccessResourcesOfDomainToBeMoved(newPathOfDomainToBeMoved, domainToBeMoved, newParentDomain, idsOfDomainsOfNetworkOfferingsUsedByDomainToBeMoved, "Network offerings"); + + Map> idsOfDomainsOfDedicatedResourcesUsedByDomainToBeMoved = + _dedicatedDao.listDomainsOfDedicatedResourcesUsedByDomainPath(currentPathOfDomainToBeMoved); + validateNewParentDomainCanAccessResourcesOfDomainToBeMoved(newPathOfDomainToBeMoved, domainToBeMoved, newParentDomain, idsOfDomainsOfDedicatedResourcesUsedByDomainToBeMoved, "Dedicated resources"); + } + + protected void validateNewParentDomainCanAccessResourcesOfDomainToBeMoved(String newPathOfDomainToBeMoved, DomainVO domainToBeMoved, DomainVO newParentDomain, + Map> idsOfDomainsWithResourcesUsedByDomainToBeMoved, String resourceToLog) { + Map> domainsOfResourcesInaccessibleToNewParentDomain = new HashMap<>(); + for (Map.Entry> entry : idsOfDomainsWithResourcesUsedByDomainToBeMoved.entrySet()) { + DomainVO domainWithResourceUsedByDomainToBeMoved = _domainDao.findById(entry.getKey()); + + Pattern pattern = Pattern.compile(domainWithResourceUsedByDomainToBeMoved.getPath().replace("/", "\\/").concat(".*")); + Matcher matcher = pattern.matcher(newPathOfDomainToBeMoved); + if (!matcher.matches()) { + domainsOfResourcesInaccessibleToNewParentDomain.put(domainWithResourceUsedByDomainToBeMoved, entry.getValue()); + } + } + + if (!domainsOfResourcesInaccessibleToNewParentDomain.isEmpty()) { + s_logger.error(String.format("The new parent domain [%s] does not have access to domains [%s] used by [%s] in the domain to be moved [%s].", + newParentDomain, domainsOfResourcesInaccessibleToNewParentDomain.keySet(), domainsOfResourcesInaccessibleToNewParentDomain.values(), domainToBeMoved)); + throw new InvalidParameterValueException(String.format("New parent domain [%s] does not have access to [%s] used by domain [%s], therefore, domain [%s] cannot be moved.", + newParentDomain, resourceToLog, domainToBeMoved, domainToBeMoved)); + } + } + + protected DomainVO returnDomainIfExistsAndIsActive(Long idOfDomain) { + s_logger.debug(String.format("Checking if domain with ID [%s] exists and is active.", idOfDomain)); + DomainVO domain = _domainDao.findById(idOfDomain); + + if (domain == null) { + throw new InvalidParameterValueException(String.format("Unable to find a domain with the specified ID [%s].", idOfDomain)); + } else if (domain.getState().equals(Domain.State.Inactive)) { + throw new InvalidParameterValueException(String.format("Unable to use the domain [%s] as it is in state [%s].", domain, Domain.State.Inactive)); + } + + return domain; + } + + protected void updateDomainAndChildrenPathAndLevel(DomainVO domainToBeMoved, DomainVO newParentDomain, String oldPath, String newPath) { + Integer oldRootLevel = domainToBeMoved.getLevel(); + Integer newLevel = newParentDomain.getLevel() + 1; + + updateDomainPathAndLevel(domainToBeMoved, oldPath, newPath, oldRootLevel, newLevel); + + List childrenDomain = _domainDao.findAllChildren(oldPath, domainToBeMoved.getId()); + for (DomainVO childDomain : childrenDomain) { + updateDomainPathAndLevel(childDomain, oldPath, newPath, oldRootLevel, newLevel); + } + } + + protected void updateDomainPathAndLevel(DomainVO domain, String oldPath, String newPath, Integer oldRootLevel, Integer newLevel) { + String finalPath = StringUtils.replaceOnce(domain.getPath(), oldPath, newPath); + domain.setPath(finalPath); + + Integer currentLevel = domain.getLevel(); + int finalLevel = newLevel + currentLevel - oldRootLevel; + domain.setLevel(finalLevel); + + s_logger.debug(String.format("Updating the path to [%s] and the level to [%s] of the domain [%s].", finalPath, finalLevel, domain)); + _domainDao.update(domain.getId(), domain); + } + + protected void updateResourceCounts(Long idOfOldParentDomain, Long idOfNewParentDomain) { + s_logger.debug(String.format("Updating the resource counts of the old parent domain [%s] and of the new parent domain [%s].", idOfOldParentDomain, idOfNewParentDomain)); + resourceLimitService.recalculateResourceCount(null, idOfOldParentDomain, null); + resourceLimitService.recalculateResourceCount(null, idOfNewParentDomain, null); + } + + protected void updateChildCounts(DomainVO oldParentDomain, DomainVO newParentDomain) { + int finalOldParentChildCount = oldParentDomain.getChildCount() - 1; + + oldParentDomain.setChildCount(finalOldParentChildCount); + oldParentDomain.setNextChildSeq(finalOldParentChildCount + 1); + + s_logger.debug(String.format("Updating the child count of the old parent domain [%s] to [%s].", oldParentDomain, finalOldParentChildCount)); + _domainDao.update(oldParentDomain.getId(), oldParentDomain); + + int finalNewParentChildCount = newParentDomain.getChildCount() + 1; + + newParentDomain.setChildCount(finalNewParentChildCount); + newParentDomain.setNextChildSeq(finalNewParentChildCount + 1); + + s_logger.debug(String.format("Updating the child count of the new parent domain [%s] to [%s].", newParentDomain, finalNewParentChildCount)); + _domainDao.update(newParentDomain.getId(), newParentDomain); + } }