diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java index 284b91e1346..ded9440613e 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -75,7 +75,6 @@ public interface DnsProviderManager extends Manager, PluggableService { boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd); - void addDnsRecordForVM(VirtualMachine instance, Network network, Nic nic); void deleteDnsRecordForVM(VirtualMachine instance, Network network, Nic nic); void checkDnsServerPermission(Account caller, DnsServer dnsServer); diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 1f713e26efc..61fb30d8657 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -317,5 +317,6 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 433c8a891ae..c891a2ca875 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -124,7 +124,7 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics','enabled', 'TINYINT(1) NOT NUL -- ====================================================================== -- DNS Server Table (Stores DNS Server Configurations) -CREATE TABLE `cloud`.`dns_server` ( +CREATE TABLE IF NOT EXISTS `cloud`.`dns_server` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns server', `uuid` varchar(40) COMMENT 'uuid of the dns server', `name` varchar(255) NOT NULL COMMENT 'display name of the dns server', @@ -148,7 +148,7 @@ CREATE TABLE `cloud`.`dns_server` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- DNS Zone Table (Stores DNS Zone Metadata) -CREATE TABLE `cloud`.`dns_zone` ( +CREATE TABLE IF NOT EXISTS `cloud`.`dns_zone` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone', `uuid` varchar(40) COMMENT 'uuid of the dns zone', `name` varchar(255) NOT NULL COMMENT 'dns zone name (e.g. example.com)', @@ -171,7 +171,7 @@ CREATE TABLE `cloud`.`dns_zone` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- DNS Zone Network Map (One-to-Many Link) -CREATE TABLE `cloud`.`dns_zone_network_map` ( +CREATE TABLE IF NOT EXISTS `cloud`.`dns_zone_network_map` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone to network mapping', `uuid` varchar(40), `dns_zone_id` bigint(20) unsigned NOT NULL, diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_nic_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_nic_view.sql new file mode 100644 index 00000000000..6506434a319 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_nic_view.sql @@ -0,0 +1,40 @@ +-- 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. + +-- VIEW `cloud`.`dns_nic_view`; + +DROP VIEW IF EXISTS `cloud`.`dns_nic_view`; +CREATE VIEW `cloud`.`dns_nic_view` AS +SELECT + n.id AS id, + n.uuid AS uuid, + n.instance_id AS instance_id, + n.network_id AS network_id, + n.ip4_address AS ip4_address, + n.ip6_address AS ip6_address, + n.removed AS removed, + nd.value AS nic_dns_url, + map.dns_zone_id AS dns_zone_id, + map.sub_domain AS sub_domain +FROM + `cloud`.`nics` n + INNER JOIN + `cloud`.`dns_zone_network_map` map ON n.network_id = map.network_id + LEFT JOIN + `cloud`.`nic_details` nd ON n.id = nd.nic_id AND nd.name = 'nicdnsrecord' +WHERE + map.removed IS NULL; diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java index 34635d7e957..a9b2581cb13 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -56,6 +56,7 @@ import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.dao.DnsNicJoinDao; import org.apache.cloudstack.dns.dao.DnsServerDao; import org.apache.cloudstack.dns.dao.DnsServerJoinDao; import org.apache.cloudstack.dns.dao.DnsZoneDao; @@ -63,7 +64,9 @@ import org.apache.cloudstack.dns.dao.DnsZoneJoinDao; import org.apache.cloudstack.dns.dao.DnsZoneNetworkMapDao; import org.apache.cloudstack.dns.exception.DnsConflictException; import org.apache.cloudstack.dns.exception.DnsNotFoundException; +import org.apache.cloudstack.dns.exception.DnsProviderException; import org.apache.cloudstack.dns.exception.DnsTransportException; +import org.apache.cloudstack.dns.vo.DnsNicJoinVO; import org.apache.cloudstack.dns.vo.DnsServerJoinVO; import org.apache.cloudstack.dns.vo.DnsServerVO; import org.apache.cloudstack.dns.vo.DnsZoneJoinVO; @@ -73,7 +76,6 @@ import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.MessageSubscriber; import org.apache.cloudstack.framework.messagebus.PublishScope; import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.RandomStringUtils; import org.apache.logging.log4j.util.Strings; import org.springframework.stereotype.Component; @@ -95,11 +97,11 @@ import com.cloud.utils.component.PluggableService; import com.cloud.utils.db.Filter; import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn; +import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.Nic; import com.cloud.vm.NicDetailVO; -import com.cloud.vm.NicVO; -import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.dao.NicDao; @@ -138,6 +140,8 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa MessageBus messageBus; @Inject VMInstanceDao vmInstanceDao; + @Inject + DnsNicJoinDao dnsNicJoinDao; private DnsProvider getProviderByType(DnsProviderType type) { if (type == null) { @@ -701,57 +705,6 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa return dnsZoneNetworkMapDao.remove(mapping.getId()); } - @Override - public void addDnsRecordForVM(VirtualMachine instance, Network network, Nic nic) { - DnsZoneNetworkMapVO dnsZoneNetworkMap = dnsZoneNetworkMapDao.findByNetworkId(network.getId()); - if (dnsZoneNetworkMap == null) { - logger.warn("No DNS zone is mapped to this network. Please associate a zone first."); - return; - } - DnsZoneVO dnsZone = dnsZoneDao.findById(dnsZoneNetworkMap.getDnsZoneId()); - if (dnsZone == null || dnsZone.getState() != DnsZone.State.Active) { - logger.warn("DNS zone is not available for DNS record setup"); - return; - } - DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId()); - if (server == null) { - logger.warn("DNS server is not found to process DNS record for Instance: {}", instance.getInstanceName()); - return; - } - String recordName = finalizeDnsRecordNameForVm(instance, dnsZoneNetworkMap, server, dnsZone); - String dnsRecordUrl = processDnsRecordInProvider(recordName, instance, server, dnsZone, nic, true); - if (Strings.isBlank(dnsRecordUrl)) { - logger.error("Failed to add DNS record in provider for Instance: {}", instance.getInstanceName()); - return; - } - nicDetailsDao.addDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD, dnsRecordUrl, true); - } - - private String finalizeDnsRecordNameForVm(VirtualMachine instance, DnsZoneNetworkMapVO dnsZoneNetworkMap, DnsServerVO server, DnsZoneVO dnsZone) { - String recordName; - // Construct FQDN Prefix (e.g., "hostname.dnsZoneName" or "hostname.subdomain.dnsZoneName") - try { - List parts = new ArrayList<>(); - parts.add(instance.getHostName()); - if (StringUtils.isNotBlank(dnsZoneNetworkMap.getSubDomain())) { - parts.add(dnsZoneNetworkMap.getSubDomain()); - } - parts.add(dnsZone.getName()); - recordName = String.join(".", parts); - - DnsProvider provider = getProviderByType(server.getProviderType()); - boolean dnsRecordExist = provider.dnsRecordExists(server, dnsZone, recordName, DnsRecord.RecordType.A.toString()); - if (dnsRecordExist) { - String randomPrefix = RandomStringUtils.randomAlphanumeric(3).toLowerCase(); - recordName = randomPrefix + "-" + recordName; - } - } catch (Exception ex) { - logger.error("Failed while constructing DNS record name for Instance: {} ", instance.getInstanceName(), ex); - throw new CloudRuntimeException("Error occurred during DNS record registration for Instance: " + instance.getInstanceName()); - } - return recordName; - } - @Override public void deleteDnsRecordForVM(VirtualMachine instance, Network network, Nic nic) { String instanceName = instance.getInstanceName(); @@ -918,11 +871,11 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa long instanceId = (long) event.get(ApiConstants.INSTANCE_ID); switch (newState) { case Running: - handleVmEvent(instanceId, true); + handleVmRunningState(instanceId); break; case Stopped: case Destroyed: - handleVmEvent(instanceId, false); + handleVmStopAndDestroy(instanceId); break; default: logger.warn("Ignoring lifecycle event for Instance ID: {}, unsupported state={}", instanceId, newState); @@ -950,9 +903,9 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa return; } if (EVENT_NIC_CREATE.equals(eventType)) { - handleNicEvent(nicId, instanceId, true); + handleNicPlug(instanceId, nicId); } else if (EVENT_NIC_DELETE.equals(eventType)) { - handleNicEvent(nicId, instanceId, false); + handleNicUnplug(instanceId, nicId); } else { logger.warn("Ignoring lifecycle event for NIC with ID: {}, unsupported eventType={}", nicId, eventType); } @@ -993,49 +946,6 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa } } - private void handleNicEvent(long nicId, long instanceId, boolean isAddDnsRecord) { - VMInstanceVO vmInstanceVO = vmInstanceDao.findById(instanceId); - if (vmInstanceVO == null) { - logger.error("Unable to find Instance with ID: {}", instanceId); - return; - } - Nic nic = nicDao.findByIdIncludingRemoved(nicId); - if (nic == null) { - logger.error("NIC is not found for the ID: {}", nicId); - return; - } - Network network = networkDao.findById(nic.getNetworkId()); - if (network == null || !Network.GuestType.Shared.equals(network.getGuestType())) { - logger.warn("Network is not eligible for DNS record registration"); - return; - } - processEventForDnsRecord(vmInstanceVO, network, nic, isAddDnsRecord); - } - - private void handleVmEvent(long instanceId, boolean isAddDnsRecord) { - VMInstanceVO vmInstanceVO = vmInstanceDao.findByIdIncludingRemoved(instanceId); - if (vmInstanceVO == null) { - logger.error("Unable to find Instance with ID: {}", instanceId); - return; - } - List vmNics = nicDao.listByVmIdIncludingRemoved(vmInstanceVO.getId()); - for (NicVO nic : vmNics) { - Network network = networkDao.findById(nic.getNetworkId()); - if (network == null || !Network.GuestType.Shared.equals(network.getGuestType())) { - continue; - } - processEventForDnsRecord(vmInstanceVO, network, nic, isAddDnsRecord); - } - } - - void processEventForDnsRecord(VMInstanceVO vmInstanceVO, Network network, Nic nic, boolean isAddDnsRecord) { - if (isAddDnsRecord) { - addDnsRecordForVM(vmInstanceVO, network, nic); - } else { - deleteDnsRecordForVM(vmInstanceVO, network, nic); - } - } - void publishDnsRecordEventMessageBus(String dnsRecord, DnsRecord.RecordType recordType, Long accountId, String eventType, List contents) { @@ -1056,4 +966,215 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa logger.error("Failed to publish {} event for DNS record: {}", eventType, dnsRecord, ex); } } + + void handleVmRunningState(long instanceId) { + VirtualMachine instance = vmInstanceDao.findById(instanceId); + if (instance == null) { + logger.debug("Instance is not found for the given ID: {}", instanceId); + return; + } + List mappedNics = dnsNicJoinDao.listActiveByVmId(instanceId); + if (CollectionUtils.isEmpty(mappedNics)) { + logger.debug("No active DNS zone associated to NICs"); + return; + } + Map>> dnsZoneRecordNicMap = new HashMap<>(); + for (DnsNicJoinVO nic : mappedNics) { + DnsZoneVO targetZone = dnsZoneDao.findById(nic.getDnsZoneId()); + if (targetZone == null) { + continue; + } + String targetDnsRecordUrl = prepareDnsRecordUrl(instance.getHostName(), nic.getSubDomain(), targetZone.getName()); + dnsZoneRecordNicMap.computeIfAbsent(targetZone.getId(), k -> new HashMap<>()) + .computeIfAbsent(targetDnsRecordUrl, k -> new ArrayList<>()) + .add(nic); + } + + for (Map.Entry>> zoneEntry : dnsZoneRecordNicMap.entrySet()) { + long targetZoneId = zoneEntry.getKey(); + for (Map.Entry> dnsUrlEntry : zoneEntry.getValue().entrySet()) { + String dnsRecordUrl = dnsUrlEntry.getKey(); + List nicsForThisFqdn = dnsUrlEntry.getValue(); + try { + Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException{ + DnsNicJoinVO existing = dnsNicJoinDao.findActiveByDnsRecordAndZone(dnsRecordUrl, targetZoneId); + if (existing != null && existing.getInstanceId() != instanceId) { + logger.error("DNS Collision: Cannot register DNS record: {}. Already owned by Instance: {}.", + dnsRecordUrl, existing.getInstanceId()); + return; + } + for (DnsNicJoinVO nic : nicsForThisFqdn) { + nicDetailsDao.addDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD, dnsRecordUrl, true); + } + syncDnsState(instanceId, dnsRecordUrl, targetZoneId); + } + }); + } catch (DnsProviderException ex) { + logger.error("Failed to register DNS record: {} in provider for Instance: {}", dnsRecordUrl, instance.getUuid(), ex); + } catch (Exception ex) { + logger.warn("Failed to register DNS record: {} for Instance: {}", dnsRecordUrl, instance.getUuid(), ex); + } + } + } + } + + void handleVmStopAndDestroy(long instanceId) { + List historicalNics = dnsNicJoinDao.listIncludingRemovedByVmId(instanceId); + if (CollectionUtils.isEmpty(historicalNics)) { + return; + } + Map>> groupByDnsZone = new HashMap<>(); + for (DnsNicJoinVO nic : historicalNics) { + // If the DNS record url is null, it means this NIC was never registered in nic_details + if (nic.getNicDnsUrl() == null) { + continue; + } + groupByDnsZone + .computeIfAbsent(nic.getDnsZoneId(), k -> new HashMap<>()) + .computeIfAbsent(nic.getNicDnsUrl(), k -> new ArrayList<>()) + .add(nic); + } + + for (Map.Entry>> zoneEntry : groupByDnsZone.entrySet()) { + long targetZoneId = zoneEntry.getKey(); + for (Map.Entry> dnsRecordEntry : zoneEntry.getValue().entrySet()) { + String dnsRecordUrl = dnsRecordEntry.getKey(); + List nicsForDnsUrl = dnsRecordEntry.getValue(); + + try { + Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException { + for (DnsNicJoinVO nic : nicsForDnsUrl) { + nicDetailsDao.removeDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD); + } + // Because we just deleted the nic_details, the sync method will naturally + // find 0 active IPs for this VM/FQDN combo and issue a clean DELETE to PowerDNS. + syncDnsState(instanceId, dnsRecordUrl, targetZoneId); + } + }); + logger.debug("Successfully cleaned up DNS record: {} for Instance with ID: {}", dnsRecordUrl, instanceId); + } catch (DnsProviderException ex) { + logger.error("Failed to cleanup DNS record: {} in provider", dnsRecordUrl, ex); + } catch (Exception ex) { + logger.error("Failed DNS record: {} cleanup for Instance", dnsRecordUrl, ex); + } + } + } + } + + void handleNicPlug(long instanceId, long nicId) { + VirtualMachine instance = vmInstanceDao.findById(instanceId); + if (instance == null || instance.getState() != VirtualMachine.State.Running) { + return; + } + DnsNicJoinVO nic = dnsNicJoinDao.findById(nicId); + if (nic == null) { + logger.debug("NIC with ID: {} doesn't have DNS zone associated", nicId); + return; + } + + DnsZoneVO targetZone = dnsZoneDao.findById(nic.getDnsZoneId()); + if (targetZone == null) { + return; + } + + String dnsRecordUrl = prepareDnsRecordUrl(instance.getHostName(), nic.getSubDomain(), targetZone.getName()); + try { + Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { + + @Override + public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException { + DnsNicJoinVO existing = dnsNicJoinDao.findActiveByDnsRecordAndZone(dnsRecordUrl, targetZone.getId()); + if (existing != null && existing.getInstanceId() != instanceId) { + logger.error("DNS Collision: Cannot register DNS record: {}. Already owned by Instance: {}.", + dnsRecordUrl, existing.getInstanceId()); + return; + } + nicDetailsDao.addDetail(nicId, ApiConstants.NIC_DNS_RECORD, dnsRecordUrl, true); + syncDnsState(instanceId, dnsRecordUrl, targetZone.getId()); + logger.debug("Successfully synced DNS on NIC Plug for: {}", dnsRecordUrl); + } + }); + } catch (DnsProviderException ex) { + logger.error("Failed to register DNS record: {} in provider for Instance: {}", dnsRecordUrl, instance.getUuid(), ex); + } catch (Exception ex) { + logger.warn("Failed to register DNS record: {} for Instance: {}", dnsRecordUrl, instance.getUuid(), ex); + } + } + + void handleNicUnplug(long instanceId, long nicId) { + DnsNicJoinVO nic = dnsNicJoinDao.findByIdIncludingRemoved(nicId); + if (nic == null || nic.getNicDnsUrl() == null) { + return; + } + String dnsRecordUrl = nic.getNicDnsUrl(); + long dnsZoneId = nic.getDnsZoneId(); + + try { + Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException { + nicDetailsDao.removeDetail(nicId, ApiConstants.NIC_DNS_RECORD); + syncDnsState(instanceId, dnsRecordUrl, dnsZoneId); + logger.debug("Successfully synced DNS record: {} on NIC unplug", dnsRecordUrl); + } + }); + } catch (DnsProviderException ex) { + logger.error("Failed to sync DNS record: {} on NIC unplug in provider", dnsRecordUrl); + } catch (Exception ex) { + logger.error("Failed to sync DNS record: {} on NIC unplug ", dnsRecordUrl); + } + } + + String prepareDnsRecordUrl(String hostName, String subDomain, String dnsZoneName) { + List parts = new ArrayList<>(); + parts.add(hostName); + if (StringUtils.isNotBlank(subDomain)) { + parts.add(subDomain.trim()); + } + parts.add(dnsZoneName); + return String.join(".", parts); + } + + public void syncDnsState(Long instanceId, String dnsRecordUrl, long dnsZoneId) throws DnsProviderException { + DnsZone dnsZone = dnsZoneDao.findById(dnsZoneId); + if (dnsZone == null) { + logger.error("DNS zone not found for the provided ID: {}", dnsZoneId); + return; + } + DnsServerVO dnsServer = dnsServerDao.findById(dnsZone.getDnsServerId()); + List activeNics = dnsNicJoinDao.listActiveByVmIdZoneAndDnsRecord(instanceId, dnsZoneId, dnsRecordUrl); + + List ipv4s = new ArrayList<>(); + List ipv6s = new ArrayList<>(); + for (DnsNicJoinVO nic : activeNics) { + if (nic.getIp4Address() != null && !nic.getIp4Address().isEmpty()) { + ipv4s.add(nic.getIp4Address()); + } + if (nic.getIp6Address() != null && !nic.getIp6Address().isEmpty()) { + ipv6s.add(nic.getIp6Address()); + } + } + logger.debug("Syncing DNS for: {}. Found: {} IPv4s and {} IPv6s.", dnsRecordUrl, ipv4s.size(), ipv6s.size()); + DnsProvider provider = getProviderByType(dnsServer.getProviderType()); + DnsRecord recordIpv4 = new DnsRecord(dnsRecordUrl, DnsRecord.RecordType.A, null, 3600); + // push A records into Provider + if (ipv4s.isEmpty()) { + provider.deleteRecord(dnsServer, dnsZone, recordIpv4); + } else { + recordIpv4.setContents(ipv4s); + provider.addRecord(dnsServer, dnsZone, recordIpv4); + } + // push AAAA records into Provider + DnsRecord recordIpv6 = new DnsRecord(dnsRecordUrl, DnsRecord.RecordType.AAAA, null, 3600); + if (ipv6s.isEmpty()) { + provider.deleteRecord(dnsServer, dnsZone, recordIpv6); + } else { + recordIpv6.setContents(ipv6s); + provider.addRecord(dnsServer, dnsZone, recordIpv6); + } + } } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsNicJoinDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsNicJoinDao.java new file mode 100644 index 00000000000..fc91d34df44 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsNicJoinDao.java @@ -0,0 +1,58 @@ +// 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.dns.dao; + +import java.util.List; + +import org.apache.cloudstack.dns.vo.DnsNicJoinVO; + +import com.cloud.utils.db.GenericDao; + +public interface DnsNicJoinDao extends GenericDao { + + /** + * Used for Collision Checks. + * @param dnsRecordUrl + * @param dnsZoneId + * @return active records to see who currently owns the dnsRecordUrl. + */ + DnsNicJoinVO findActiveByDnsRecordAndZone(String dnsRecordUrl, long dnsZoneId); + + /** + * Used to sync DNS record url based on available ips for vmId in the dnsZone + * @param vmId + * @param dnsZoneId + * @param dnsRecordUrl + * @return list of active nics using the dnsRecordUrl, supports null vmId for dnsZone wide query + */ + List listActiveByVmIdZoneAndDnsRecord(Long vmId, long dnsZoneId, String dnsRecordUrl); + + /** + * Used for VM Start/Running + * @param vmId + * @return records associated to vmId + */ + List listActiveByVmId(long vmId); + + /** + * Used by Instance Destroy/Stop or NIC delete + * @param vmId + * @return records with soft-delete + */ + List listIncludingRemovedByVmId(long vmId); +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsNicJoinDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsNicJoinDaoImpl.java new file mode 100644 index 00000000000..9fd718f67fe --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsNicJoinDaoImpl.java @@ -0,0 +1,91 @@ +// 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.dns.dao; + +import java.util.List; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.vo.DnsNicJoinVO; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +public class DnsNicJoinDaoImpl extends GenericDaoBase implements DnsNicJoinDao { + private final SearchBuilder activeDnsRecordZoneSearch; + private final SearchBuilder activeVmZoneDnsRecordSearch; // Route for null vmId + private final SearchBuilder activeVmSearch; + + public DnsNicJoinDaoImpl() { + + activeDnsRecordZoneSearch = createSearchBuilder(); + activeDnsRecordZoneSearch.and(ApiConstants.NIC_DNS_RECORD, activeDnsRecordZoneSearch.entity().getNicDnsUrl(), SearchCriteria.Op.EQ); + activeDnsRecordZoneSearch.and(ApiConstants.DNS_ZONE_ID, activeDnsRecordZoneSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ); + activeDnsRecordZoneSearch.and(ApiConstants.REMOVED, activeDnsRecordZoneSearch.entity().getRemoved(), SearchCriteria.Op.NULL); + activeDnsRecordZoneSearch.done(); + + activeVmZoneDnsRecordSearch = createSearchBuilder(); + activeVmZoneDnsRecordSearch.and(ApiConstants.INSTANCE_ID, activeVmZoneDnsRecordSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + activeVmZoneDnsRecordSearch.and(ApiConstants.NIC_DNS_RECORD, activeVmZoneDnsRecordSearch.entity().getNicDnsUrl(), SearchCriteria.Op.EQ); + activeVmZoneDnsRecordSearch.and(ApiConstants.DNS_ZONE_ID, activeVmZoneDnsRecordSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ); + activeVmZoneDnsRecordSearch.and(ApiConstants.REMOVED, activeVmZoneDnsRecordSearch.entity().getRemoved(), SearchCriteria.Op.NULL); + activeVmZoneDnsRecordSearch.done(); + + activeVmSearch = createSearchBuilder(); + activeVmSearch.and(ApiConstants.INSTANCE_ID, activeVmSearch.entity().getInstanceId(), SearchCriteria.Op.EQ); + activeVmSearch.done(); + } + + @Override + public DnsNicJoinVO findActiveByDnsRecordAndZone(String dnsRecordUrl, long dnsZoneId) { + SearchCriteria sc = activeDnsRecordZoneSearch.create(); + sc.setParameters(ApiConstants.NIC_DNS_RECORD, dnsRecordUrl); + sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId); + return findOneBy(sc); + } + + @Override + public List listActiveByVmIdZoneAndDnsRecord(Long vmId, long dnsZoneId, String dnsRecordUrl) { + if (vmId != null) { + SearchCriteria sc = activeDnsRecordZoneSearch.create(); + sc.setParameters(ApiConstants.INSTANCE_ID, vmId); + sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId); + sc.setParameters(ApiConstants.NIC_DNS_RECORD, dnsRecordUrl); + return listBy(sc); + } else { + SearchCriteria sc = activeDnsRecordZoneSearch.create(); + sc.setParameters(ApiConstants.NIC_DNS_RECORD, dnsRecordUrl); + sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId); + return listBy(sc); + } + } + + @Override + public List listActiveByVmId(long vmId) { + SearchCriteria sc = activeVmSearch.create(); + sc.setParameters(ApiConstants.INSTANCE_ID, vmId); + return listBy(sc); + } + + @Override + public List listIncludingRemovedByVmId(long vmId) { + SearchCriteria sc = activeVmSearch.create(); + sc.setParameters(ApiConstants.INSTANCE_ID, vmId); + return listIncludingRemovedBy(sc); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsNicJoinVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsNicJoinVO.java new file mode 100644 index 00000000000..7396c86949f --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsNicJoinVO.java @@ -0,0 +1,115 @@ +/* + * 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.dns.vo; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import com.cloud.api.query.vo.BaseViewVO; + +@Entity +@Table(name = "dns_nic_view") +public class DnsNicJoinVO extends BaseViewVO implements InternalIdentity, Identity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "instance_id") + private long instanceId; + + @Column(name = "network_id") + private long networkId; + + @Column(name = "ip4_address") + private String ip4Address; + + @Column(name = "ip6_address") + private String ip6Address; + + @Column(name = "nic_dns_url") + private String nicDnsUrl; + + @Column(name = "dns_zone_id") + private long dnsZoneId; + + @Column(name = "sub_domain") + private String subDomain; + + @Column(name = "removed") + private Date removed; + + public DnsNicJoinVO() { + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public long getInstanceId() { + return instanceId; + } + + public long getNetworkId() { + return networkId; + } + + public long getDnsZoneId() { + return dnsZoneId; + } + + public String getSubDomain() { + return subDomain; + } + + public String getNicDnsUrl() { + return nicDnsUrl; + } + + public String getIp4Address() { + return ip4Address; + } + + public String getIp6Address() { + return ip6Address; + } + + public Date getRemoved() { + return removed; + } +} diff --git a/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java index 81117f38983..9517e5e6bb8 100644 --- a/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java @@ -52,6 +52,7 @@ import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.dao.DnsNicJoinDao; import org.apache.cloudstack.dns.dao.DnsServerDao; import org.apache.cloudstack.dns.dao.DnsServerJoinDao; import org.apache.cloudstack.dns.dao.DnsZoneDao; @@ -59,7 +60,6 @@ import org.apache.cloudstack.dns.dao.DnsZoneJoinDao; import org.apache.cloudstack.dns.dao.DnsZoneNetworkMapDao; import org.apache.cloudstack.dns.exception.DnsConflictException; import org.apache.cloudstack.dns.exception.DnsNotFoundException; -import org.apache.cloudstack.dns.exception.DnsProviderException; import org.apache.cloudstack.dns.exception.DnsTransportException; import org.apache.cloudstack.dns.vo.DnsServerJoinVO; import org.apache.cloudstack.dns.vo.DnsServerVO; @@ -130,6 +130,8 @@ public class DnsProviderManagerImplTest { @Mock NicDetailsDao nicDetailsDao; @Mock + DnsNicJoinDao dnsNicJoinDao; + @Mock MessageBus messageBus; @Mock VMInstanceDao vmInstanceDao; @@ -571,51 +573,6 @@ public class DnsProviderManagerImplTest { manager.checkDnsZonePermission(callerMock, zoneVO); } - @Test - public void testAddDnsRecordForVMNoNetworkMapping() throws DnsProviderException { - Network network = mock(Network.class); - NicVO nic = mock(NicVO.class); - VMInstanceVO vm = mock(VMInstanceVO.class); - when(dnsZoneNetworkMapDao.findByNetworkId(anyLong())).thenReturn(null); - when(network.getId()).thenReturn(NETWORK_ID); - manager.addDnsRecordForVM(vm, network, nic); - verify(dnsProviderMock, never()).addRecord(any(), any(), any()); - } - - @Test - public void testAddDnsRecordForVMInactiveZone() { - Network network = mock(Network.class); - NicVO nic = mock(NicVO.class); - VMInstanceVO vm = mock(VMInstanceVO.class); - DnsZoneNetworkMapVO mapping = mock(DnsZoneNetworkMapVO.class); - when(network.getId()).thenReturn(NETWORK_ID); - when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(mapping); - when(mapping.getDnsZoneId()).thenReturn(ZONE_ID); - DnsZoneVO inactiveZone = Mockito.spy(new DnsZoneVO("ex.com", DnsZone.ZoneType.Public, SERVER_ID, ACCOUNT_ID, DOMAIN_ID, "")); - // state defaults to Inactive - when(dnsZoneDao.findById(ZONE_ID)).thenReturn(inactiveZone); - manager.addDnsRecordForVM(vm, network, nic); - verify(dnsServerDao, never()).findById(anyLong()); - } - - @Test - public void testAddDnsRecordForVMServerMissing() { - Network network = mock(Network.class); - NicVO nic = mock(NicVO.class); - VMInstanceVO vm = mock(VMInstanceVO.class); - DnsZoneNetworkMapVO mapping = mock(DnsZoneNetworkMapVO.class); - when(network.getId()).thenReturn(NETWORK_ID); - when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(mapping); - when(mapping.getDnsZoneId()).thenReturn(ZONE_ID); - DnsZoneVO activeZone = Mockito.spy(new DnsZoneVO("ex.com", DnsZone.ZoneType.Public, SERVER_ID, ACCOUNT_ID, DOMAIN_ID, "")); - activeZone.setState(DnsZone.State.Active); - when(dnsZoneDao.findById(ZONE_ID)).thenReturn(activeZone); - when(dnsServerDao.findById(SERVER_ID)).thenReturn(null); - when(vm.getInstanceName()).thenReturn("vm-1"); - manager.addDnsRecordForVM(vm, network, nic); - verify(nicDetailsDao, never()).addDetail(anyLong(), anyString(), anyString(), eq(true)); - } - @Test public void testDeleteDnsRecordForVMNoNicDetail() { Network network = mock(Network.class); @@ -642,32 +599,6 @@ public class DnsProviderManagerImplTest { verify(dnsZoneNetworkMapDao, never()).findByNetworkId(anyLong()); } - @Test - public void testProcessEventForDnsRecordAdd() throws Exception { - Network network = mock(Network.class); - NicVO nic = mock(NicVO.class); - VMInstanceVO vm = mock(VMInstanceVO.class); - - when(dnsZoneNetworkMapDao.findByNetworkId(anyLong())).thenReturn(null); - when(network.getId()).thenReturn(NETWORK_ID); - manager.processEventForDnsRecord(vm, network, nic, true); - // addDnsRecordForVM was called → returns early because no mapping - verify(dnsZoneNetworkMapDao, times(1)).findByNetworkId(NETWORK_ID); - } - - @Test - public void testProcessEventForDnsRecordDelete() { - Network network = mock(Network.class); - NicVO nic = mock(NicVO.class); - VMInstanceVO vm = mock(VMInstanceVO.class); - - when(nic.getId()).thenReturn(50L); - when(vm.getInstanceName()).thenReturn("vm-1"); - when(nicDetailsDao.findDetail(50L, "nicdnsrecord")).thenReturn(null); - manager.processEventForDnsRecord(vm, network, nic, false); - verify(nicDetailsDao, times(1)).findDetail(50L, "nicdnsrecord"); - } - @Test public void testGetCommandsReturnsNonEmptyList() { List> commands = manager.getCommands(); @@ -820,31 +751,6 @@ public class DnsProviderManagerImplTest { verify(messageBus, times(3)).subscribe(anyString(), any()); } - @Test - public void testHandleVmEventAndNicEvent() throws Exception { - VMInstanceVO vm = mock(VMInstanceVO.class); - NicVO nic = mock(NicVO.class); - NetworkVO network = mock(NetworkVO.class); - when(network.getId()).thenReturn(NETWORK_ID); - - when(vmInstanceDao.findById(10L)).thenReturn(vm); - when(nicDao.findByIdIncludingRemoved(50L)).thenReturn(nic); - when(nic.getNetworkId()).thenReturn(NETWORK_ID); - when(networkDao.findById(NETWORK_ID)).thenReturn(network); - when(network.getGuestType()).thenReturn(Network.GuestType.Shared); - when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(null); - - org.springframework.test.util.ReflectionTestUtils.invokeMethod(manager, "handleNicEvent", 50L, 10L, true); - verify(dnsZoneNetworkMapDao, times(1)).findByNetworkId(NETWORK_ID); - - when(vmInstanceDao.findByIdIncludingRemoved(10L)).thenReturn(vm); - when(vm.getId()).thenReturn(10L); - when(nicDao.listByVmIdIncludingRemoved(10L)).thenReturn(Collections.singletonList(nic)); - - org.springframework.test.util.ReflectionTestUtils.invokeMethod(manager, "handleVmEvent", 10L, true); - verify(dnsZoneNetworkMapDao, times(2)).findByNetworkId(NETWORK_ID); - } - @Test public void testAddDnsServerSuccess() throws Exception { org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class); @@ -1011,10 +917,10 @@ public class DnsProviderManagerImplTest { event.put(org.apache.cloudstack.api.ApiConstants.INSTANCE_ID, 12L); // Expect handleVmEvent to be called, which accesses vmInstanceDao.findByIdIncludingRemoved - when(vmInstanceDao.findByIdIncludingRemoved(12L)).thenReturn(null); + when(vmInstanceDao.findById(12L)).thenReturn(null); subscriber.onPublishMessage("sender", "subject", event); - verify(vmInstanceDao, times(1)).findByIdIncludingRemoved(12L); + verify(vmInstanceDao, times(1)).findById(12L); } @Test @@ -1024,11 +930,9 @@ public class DnsProviderManagerImplTest { event.put(org.apache.cloudstack.api.ApiConstants.OLD_STATE, com.cloud.vm.VirtualMachine.State.Running); event.put(org.apache.cloudstack.api.ApiConstants.NEW_STATE, com.cloud.vm.VirtualMachine.State.Stopped); event.put(org.apache.cloudstack.api.ApiConstants.INSTANCE_ID, 15L); - - when(vmInstanceDao.findByIdIncludingRemoved(15L)).thenReturn(null); - + when(dnsNicJoinDao.listIncludingRemovedByVmId(15L)).thenReturn(null); subscriber.onPublishMessage("sender", "subject", event); - verify(vmInstanceDao, times(1)).findByIdIncludingRemoved(15L); + verify(dnsNicJoinDao, times(1)).listIncludingRemovedByVmId(15L); } @Test @@ -1073,11 +977,9 @@ public class DnsProviderManagerImplTest { event.put(org.apache.cloudstack.api.ApiConstants.EVENT_TYPE, com.cloud.event.EventTypes.EVENT_NIC_DELETE); event.put(org.apache.cloudstack.api.ApiConstants.NIC_ID, 101L); event.put(org.apache.cloudstack.api.ApiConstants.INSTANCE_ID, 201L); - - when(vmInstanceDao.findById(201L)).thenReturn(null); - + when(dnsNicJoinDao.findByIdIncludingRemoved(101L)).thenReturn(null); subscriber.onPublishMessage("sender", "subject", event); - verify(vmInstanceDao, times(1)).findById(201L); + verify(dnsNicJoinDao, times(1)).findByIdIncludingRemoved(101L); } @Test