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