diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 0537dd3c58c..c064e680366 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -877,6 +877,7 @@ public class EventTypes { public static final String EVENT_DNS_ZONE_DELETE = "DNS.ZONE.DELETE"; public static final String EVENT_DNS_RECORD_CREATE = "DNS.RECORD.CREATE"; public static final String EVENT_DNS_RECORD_DELETE = "DNS.RECORD.DELETE"; + public static final String EVENT_DNS_NAME_COLLISION = "DNS.NAME.COLLISION"; static { diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 9b664393cf5..b664f7c3bb2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1376,6 +1376,7 @@ public class ApiConstants { public static final String INSTANCE_ID = "instanceId"; public static final String OLD_STATE = "oldState"; public static final String NEW_STATE = "newState"; + public static final String OLD_HOST_NAME = "oldHostName"; public static final String PARAMETER_DESCRIPTION_ACTIVATION_RULE = "Quota tariff's activation rule. It can receive a JS script that results in either " + diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java index 1d441b01931..65de3ed6205 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -24,11 +24,6 @@ import org.apache.cloudstack.dns.exception.DnsProviderException; import com.cloud.utils.component.Adapter; public interface DnsProvider extends Adapter { - - interface Topics { - String DNS_RECORD_LIFECYCLE = "dns.record.lifecycle"; - } - DnsProviderType getProviderType(); // Validates connectivity to the server 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 ded9440613e..f1a0de43d40 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -37,12 +37,9 @@ import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; -import com.cloud.network.Network; import com.cloud.user.Account; import com.cloud.utils.component.Manager; import com.cloud.utils.component.PluggableService; -import com.cloud.vm.Nic; -import com.cloud.vm.VirtualMachine; public interface DnsProviderManager extends Manager, PluggableService { @@ -75,8 +72,6 @@ public interface DnsProviderManager extends Manager, PluggableService { boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd); - void deleteDnsRecordForVM(VirtualMachine instance, Network network, Nic nic); - void checkDnsServerPermission(Account caller, DnsServer dnsServer); void checkDnsZonePermission(Account caller, DnsZone dnsZone); diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index 2ef19a5ea98..b22b713afcb 100644 --- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -112,7 +112,8 @@ public interface VirtualMachineManager extends Manager { interface Topics { String VM_POWER_STATE = "vm.powerstate"; - String VM_LIFECYCLE = "vm.lifecycle"; + String VM_LIFECYCLE_STATE = "vm.lifecycle.state"; + String VM_ACTION = "vm.action"; } /** diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 5dade1bb93c..fee6b76cc85 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -18,10 +18,12 @@ package com.cloud.vm; import static com.cloud.event.EventTypes.EVENT_NIC_CREATE; import static com.cloud.event.EventTypes.EVENT_NIC_DELETE; +import static com.cloud.event.EventTypes.EVENT_VM_UPDATE; import static com.cloud.hypervisor.Hypervisor.HypervisorType.Functionality; import static com.cloud.storage.Volume.IOPS_LIMIT; import static com.cloud.utils.NumbersUtil.toHumanReadableSize; -import static com.cloud.vm.VirtualMachineManager.Topics.VM_LIFECYCLE; +import static com.cloud.vm.VirtualMachineManager.Topics.VM_ACTION; +import static com.cloud.vm.VirtualMachineManager.Topics.VM_LIFECYCLE_STATE; import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS; import static org.apache.cloudstack.api.ApiConstants.MIN_IOPS; @@ -163,6 +165,7 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.logging.log4j.util.Strings; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -1554,7 +1557,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir event.put(ApiConstants.OLD_STATE, oldState != null ? oldState : State.Unknown); event.put(ApiConstants.NEW_STATE, newState); event.put(ApiConstants.TIME_STAMP, System.currentTimeMillis()); - messageBus.publish(_name, VM_LIFECYCLE, PublishScope.GLOBAL, event); + messageBus.publish(_name, VM_LIFECYCLE_STATE, PublishScope.GLOBAL, event); } catch (Exception ex) { logger.warn("Failed to publish lifecycle event for Instance: {}", instance.getUuid(), ex); } @@ -1576,6 +1579,24 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } } + private void publishVmHostNameUpdateMessageBus(long instanceId, String oldHostName, String hostName) { + if (Strings.isBlank(hostName) || oldHostName.equalsIgnoreCase(hostName)) { + return; + } + try { + Map event = new HashMap<>(); + event.put(ApiConstants.EVENT_ID, UUID.randomUUID().toString()); + event.put(ApiConstants.INSTANCE_ID, instanceId); + event.put(ApiConstants.OLD_HOST_NAME, oldHostName); + event.put(ApiConstants.HOST_NAME, hostName); + event.put(ApiConstants.EVENT_TYPE, EVENT_VM_UPDATE); + event.put(ApiConstants.TIME_STAMP, System.currentTimeMillis()); + messageBus.publish(_name, VM_ACTION, PublishScope.GLOBAL, event); + } catch (Exception ex) { + logger.error("Failed to publish Instance action event for ID: {}", instanceId, ex); + } + } + /** * Set NIC as default if VM has no default NIC * @param vmInstance VM instance to be checked @@ -2964,7 +2985,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } @Override - @ActionEvent(eventType = EventTypes.EVENT_VM_UPDATE, eventDescription = "updating Vm") + @ActionEvent(eventType = EVENT_VM_UPDATE, eventDescription = "updating Vm") public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException { validateInputsAndPermissionForUpdateVirtualMachineCommand(cmd); @@ -3340,6 +3361,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } if (State.Running == vm.getState()) { + publishVmHostNameUpdateMessageBus(vm.getId(), vm.getHostName(), hostName); updateDns(vm, hostName); } 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 a9b2581cb13..b0071a17d6f 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -17,24 +17,23 @@ package org.apache.cloudstack.dns; -import static com.cloud.event.EventTypes.EVENT_DNS_RECORD_CREATE; -import static com.cloud.event.EventTypes.EVENT_DNS_RECORD_DELETE; import static com.cloud.event.EventTypes.EVENT_NIC_CREATE; import static com.cloud.event.EventTypes.EVENT_NIC_DELETE; +import static com.cloud.event.EventTypes.EVENT_VM_UPDATE; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; import org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd; @@ -74,21 +73,21 @@ import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; import org.apache.cloudstack.dns.vo.DnsZoneVO; 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.logging.log4j.util.Strings; import org.springframework.stereotype.Component; +import com.cloud.domain.Domain; import com.cloud.domain.dao.DomainDao; import com.cloud.event.ActionEvent; +import com.cloud.event.ActionEventUtils; import com.cloud.event.EventTypes; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; -import com.cloud.network.Network; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; import com.cloud.user.Account; import com.cloud.user.AccountManager; +import com.cloud.user.User; import com.cloud.user.dao.AccountDao; import com.cloud.utils.Pair; import com.cloud.utils.StringUtils; @@ -101,7 +100,6 @@ 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.VirtualMachine; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.dao.NicDao; @@ -460,13 +458,20 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa } try { DnsRecord.RecordType type = cmd.getType(); + DnsProvider provider = getProviderByType(server.getProviderType()); + if (provider.dnsRecordExists(server, dnsZone, recordName, type.name())) { + throw new CloudRuntimeException(String.format( + "DNS record '%s' of type '%s' already exists in DNS zone '%s' (provider: %s). Overwriting is not permitted.", + recordName, type.name(), dnsZone.getName(), provider.getName() + )); + } + List normalizedContents = cmd.getContents().stream() .map(value -> DnsProviderUtil.normalizeDnsRecordValue(value, type)).collect(Collectors.toList()); DnsRecord record = new DnsRecord(recordName, type, normalizedContents, cmd.getTtl()); - DnsProvider provider = getProviderByType(server.getProviderType()); + String normalizedRecordName = provider.addRecord(server, dnsZone, record); record.setName(normalizedRecordName); - publishDnsRecordEventMessageBus(recordName, type, caller.getAccountId(), EVENT_DNS_RECORD_CREATE, normalizedContents); return createDnsRecordResponse(record); } catch (Exception ex) { logger.error("Failed to add DNS record via provider", ex); @@ -477,21 +482,20 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa @Override @ActionEvent(eventType = EventTypes.EVENT_DNS_RECORD_DELETE, eventDescription = "Deleting DNS Record") public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) { - DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); - if (zone == null) { + DnsZoneVO dnsZone = dnsZoneDao.findById(cmd.getDnsZoneId()); + if (dnsZone == null) { throw new InvalidParameterValueException("DNS zone not found."); } Account caller = CallContext.current().getCallingAccount(); - accountMgr.checkAccess(caller, null, true, zone); - DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); + accountMgr.checkAccess(caller, null, true, dnsZone); + DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId()); DnsRecord.RecordType recordType = cmd.getType(); try { DnsRecord record = new DnsRecord(); record.setName(cmd.getName()); record.setType(recordType); DnsProvider provider = getProviderByType(server.getProviderType()); - String deletedDnsRecord = provider.deleteRecord(server, zone, record); - publishDnsRecordEventMessageBus(deletedDnsRecord, recordType, caller.getAccountId(), EVENT_DNS_RECORD_DELETE, null); + String deletedDnsRecord = provider.deleteRecord(server, dnsZone, record); return deletedDnsRecord != null; } catch (Exception ex) { logger.error("Failed to delete DNS record via provider", ex); @@ -705,77 +709,6 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa return dnsZoneNetworkMapDao.remove(mapping.getId()); } - @Override - public void deleteDnsRecordForVM(VirtualMachine instance, Network network, Nic nic) { - String instanceName = instance.getInstanceName(); - NicDetailVO nicDetailVO = nicDetailsDao.findDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD); - if (nicDetailVO == null || Strings.isBlank(nicDetailVO.getValue())) { - logger.debug("No DNS record found for Instance: {}", instance.getInstanceName()); - return; - } - String dnsRecord = nicDetailVO.getValue(); - try { - DnsZoneNetworkMapVO dnsZoneNetworkMap = dnsZoneNetworkMapDao.findByNetworkId(network.getId()); - DnsZoneVO dnsZone = null; - DnsServerVO dnsServer = null; - if (dnsZoneNetworkMap != null) { - dnsZone = dnsZoneDao.findById(dnsZoneNetworkMap.getDnsZoneId()); - } - if (dnsZone != null) { - dnsServer = dnsServerDao.findById(dnsZone.getDnsServerId()); - } - if (dnsServer != null) { - processDnsRecordInProvider(dnsRecord, instance, dnsServer, dnsZone, nic, false); - } else { - logger.warn("Skipping deletion of DNS record: {} from provider for Instance: {}.", dnsRecord, instanceName); - } - } catch (Exception ex) { - logger.error("Failed deleting DNS record: {} for Instance: {}, proceeding with DB cleanup.", dnsRecord, instanceName); - } finally { - nicDetailsDao.removeDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD); - logger.debug("Removed DNS record from DB for Instance: {}, NIC ID: {}", instanceName, nic.getUuid()); - } - } - - private String processDnsRecordInProvider(String recordName, VirtualMachine instance, DnsServer server, DnsZone dnsZone, - Nic nic, boolean isAdd) { - - try { - DnsProvider provider = getProviderByType(server.getProviderType()); - // Handle IPv4 (A Record) - String ipv4DnsRecord = null; - if (nic.getIPv4Address() != null) { - DnsRecord recordA = new DnsRecord(recordName, DnsRecord.RecordType.A, Collections.singletonList(nic.getIPv4Address()), 3600); - if (isAdd) { - ipv4DnsRecord = provider.addRecord(server, dnsZone, recordA); - } else { - ipv4DnsRecord = provider.deleteRecord(server, dnsZone, recordA); - } - } - - // Handle IPv6 (AAAA Record) if it exists - String ipv6DnsRecord = null; - if (nic.getIPv6Address() != null) { - DnsRecord recordAAAA = new DnsRecord(recordName, DnsRecord.RecordType.AAAA, Collections.singletonList(nic.getIPv6Address()), 3600); - if (isAdd) { - ipv6DnsRecord = provider.addRecord(server, dnsZone, recordAAAA); - } else { - ipv6DnsRecord = provider.deleteRecord(server, dnsZone, recordAAAA); - } - } - return ipv4DnsRecord != null ? ipv4DnsRecord : ipv6DnsRecord; - } catch (Exception ex) { - logger.error( - "Failed to {} DNS record for Instance {} in zone {}", - isAdd ? "register" : "remove", - instance.getInstanceName(), - dnsZone.getName(), - ex - ); - } - return null; - } - @Override public void checkDnsServerPermission(Account caller, DnsServer dnsServer) throws PermissionDeniedException { if (caller.getId() == dnsServer.getAccountId()) { @@ -813,9 +746,9 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa @Override public boolean configure(String name, Map params) throws ConfigurationException { - messageBus.subscribe(VirtualMachineManager.Topics.VM_LIFECYCLE, new VmLifecycleSubscriber()); + messageBus.subscribe(VirtualMachineManager.Topics.VM_LIFECYCLE_STATE, new VmLifecycleSubscriber()); messageBus.subscribe(Nic.Topics.NIC_LIFECYCLE, new NicLifecycleSubscriber()); - messageBus.subscribe(DnsProvider.Topics.DNS_RECORD_LIFECYCLE, new DnsRecordLifecycleSubscriber()); + messageBus.subscribe(VirtualMachineManager.Topics.VM_ACTION, new VmRenameActionSubscriber()); return true; } @@ -916,57 +849,33 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa } } - class DnsRecordLifecycleSubscriber implements MessageSubscriber { + class VmRenameActionSubscriber implements MessageSubscriber { + @Override public void onPublishMessage(String senderAddress, String subject, Object args) { try { - logger.trace("DNS record lifecycle event: {}, {}, {}", senderAddress, subject, args); + logger.trace("VM action event: {}, {}, {}", senderAddress, subject, args); @SuppressWarnings("unchecked") Map event = (Map) args; String eventType = (String) event.get(ApiConstants.EVENT_TYPE); - String dnsRecord = (String) event.get(ApiConstants.DNS_RECORD); - if (EVENT_DNS_RECORD_CREATE.equalsIgnoreCase(eventType)) { - @SuppressWarnings("unchecked") - List contents = (List) event.get(ApiConstants.CONTENTS); - if (CollectionUtils.isNotEmpty(contents)) { - for (String ipAddress : contents) { - Nic nic = nicDao.findByIpAddressAndVmType(ipAddress, VirtualMachine.Type.User); - if (nic != null) { - nicDetailsDao.addDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD, dnsRecord, true); - } - } - } - } else if (EVENT_DNS_RECORD_DELETE.equalsIgnoreCase(eventType)) { - nicDetailsDao.removeDetailsForValuesIn(ApiConstants.NIC_DNS_RECORD, Collections.singletonList(dnsRecord)); + if (!eventType.equalsIgnoreCase(EVENT_VM_UPDATE)) { + return; } + String newHostName = (String) event.get(ApiConstants.HOST_NAME); + String oldHostName = (String) event.get(ApiConstants.OLD_HOST_NAME); + if (oldHostName.equalsIgnoreCase(newHostName)) { + logger.debug("Instance hostname is unchanged, skip event processing"); + return; + } + long instanceId = (long) event.get(ApiConstants.INSTANCE_ID); + handleVmHostnameChanged(instanceId, newHostName); } catch (Exception ex) { - logger.error("Failed to process DNS record lifecycle event", ex); + logger.error("Failed to process Instance action event", ex); } } } - void publishDnsRecordEventMessageBus(String dnsRecord, DnsRecord.RecordType recordType, Long accountId, - String eventType, List contents) { - - // Only publish for A or AAAA records and non-null record name - if ((recordType != DnsRecord.RecordType.A && recordType != DnsRecord.RecordType.AAAA) || dnsRecord == null) { - return; - } - try { - Map event = new HashMap<>(); - event.put(ApiConstants.EVENT_ID, UUID.randomUUID().toString()); - event.put(ApiConstants.DNS_RECORD, dnsRecord); - event.put(ApiConstants.ACCOUNT_ID, accountId); - event.put(ApiConstants.EVENT_TYPE, eventType); - event.put(ApiConstants.CONTENTS, contents != null ? contents : Collections.emptyList()); - event.put(ApiConstants.TIME_STAMP, System.currentTimeMillis()); - messageBus.publish(_name, DnsProvider.Topics.DNS_RECORD_LIFECYCLE, PublishScope.GLOBAL, event); - } catch (Exception ex) { - logger.error("Failed to publish {} event for DNS record: {}", eventType, dnsRecord, ex); - } - } - void handleVmRunningState(long instanceId) { VirtualMachine instance = vmInstanceDao.findById(instanceId); if (instance == null) { @@ -999,16 +908,13 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa 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()); + if (isDnsCollision(dnsRecordUrl, targetZoneId, instanceId)) { return; } for (DnsNicJoinVO nic : nicsForThisFqdn) { nicDetailsDao.addDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD, dnsRecordUrl, true); } - syncDnsState(instanceId, dnsRecordUrl, targetZoneId); + syncDnsRecordsState(instanceId, dnsRecordUrl, targetZoneId); } }); } catch (DnsProviderException ex) { @@ -1052,7 +958,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa } // 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); + syncDnsRecordsState(instanceId, dnsRecordUrl, targetZoneId); } }); logger.debug("Successfully cleaned up DNS record: {} for Instance with ID: {}", dnsRecordUrl, instanceId); @@ -1065,6 +971,78 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa } } + void handleVmHostnameChanged(long instanceId, String newHostName) { + 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)) { + return; + } + + Map>> dnsZoneNewRecordNicMap = new HashMap<>(); + for (DnsNicJoinVO nic : mappedNics) { + DnsZoneVO targetZone = dnsZoneDao.findById(nic.getDnsZoneId()); + if (targetZone == null) { + continue; + } + + String oldDnsRecordUrl = nic.getNicDnsUrl(); + String newDnsRecordUrl = prepareDnsRecordUrl(newHostName, nic.getSubDomain(), targetZone.getName()); + if (newDnsRecordUrl.equals(oldDnsRecordUrl)) { + continue; + } + + dnsZoneNewRecordNicMap.computeIfAbsent(targetZone.getId(), k -> new HashMap<>()) + .computeIfAbsent(newDnsRecordUrl, k -> new ArrayList<>()) + .add(nic); + } + + for (Map.Entry>> zoneEntry : dnsZoneNewRecordNicMap.entrySet()) { + long targetZoneId = zoneEntry.getKey(); + + for (Map.Entry> newUrlEntry : zoneEntry.getValue().entrySet()) { + String newDnsRecordUrl = newUrlEntry.getKey(); + List nicsForThisFqdn = newUrlEntry.getValue(); + + try { + Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { + @Override + public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException { + if (isDnsCollision(newDnsRecordUrl, targetZoneId, instanceId)) { + return; + } + + Set oldDnsRecordUrls = new HashSet<>(); + for (DnsNicJoinVO nic : nicsForThisFqdn) { + if (nic.getNicDnsUrl() != null) { + oldDnsRecordUrls.add(nic.getNicDnsUrl()); + } + nicDetailsDao.addDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD, newDnsRecordUrl, true); + } + + // NICs for the old URL and cleanly send a DELETE API call to PowerDNS! + for (String oldUrl : oldDnsRecordUrls) { + syncDnsRecordsState(instanceId, oldUrl, targetZoneId); + } + + // This sync call finds the newly written intent and sends an ADD/REPLACE call. + syncDnsRecordsState(instanceId, newDnsRecordUrl, targetZoneId); + } + }); + logger.debug("Successfully handled DNS Rename to: {}", newDnsRecordUrl); + + } catch (Exception ex) { + logger.error("Failed to process VM Rename for Instance: {}", instance.getUuid(), ex); + throw new CloudRuntimeException(String.format("DNS API Sync Failed for Rename to %s", newDnsRecordUrl), ex); + } + } + } + } + void handleNicPlug(long instanceId, long nicId) { VirtualMachine instance = vmInstanceDao.findById(instanceId); if (instance == null || instance.getState() != VirtualMachine.State.Running) { @@ -1087,14 +1065,11 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa @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()); + if (isDnsCollision(dnsRecordUrl, targetZone.getId(), instanceId)) { return; } nicDetailsDao.addDetail(nicId, ApiConstants.NIC_DNS_RECORD, dnsRecordUrl, true); - syncDnsState(instanceId, dnsRecordUrl, targetZone.getId()); + syncDnsRecordsState(instanceId, dnsRecordUrl, targetZone.getId()); logger.debug("Successfully synced DNS on NIC Plug for: {}", dnsRecordUrl); } }); @@ -1118,7 +1093,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa @Override public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException { nicDetailsDao.removeDetail(nicId, ApiConstants.NIC_DNS_RECORD); - syncDnsState(instanceId, dnsRecordUrl, dnsZoneId); + syncDnsRecordsState(instanceId, dnsRecordUrl, dnsZoneId); logger.debug("Successfully synced DNS record: {} on NIC unplug", dnsRecordUrl); } }); @@ -1139,7 +1114,22 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa return String.join(".", parts); } - public void syncDnsState(Long instanceId, String dnsRecordUrl, long dnsZoneId) throws DnsProviderException { + private boolean isDnsCollision(String dnsRecordUrl, long targetZoneId, long instanceId) { + 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()); + + String description = String.format("Instance hostname change resulted in a DNS collision. " + + "The requested DNS record '%s' is already in use.", dnsRecordUrl); + ActionEventUtils.onActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, Domain.ROOT_DOMAIN, + EventTypes.EVENT_DNS_NAME_COLLISION, description, instanceId, ApiCommandResourceType.VirtualMachine.toString()); + return true; + } + return false; + } + + public void syncDnsRecordsState(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); 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 9517e5e6bb8..7005d2facf5 100644 --- a/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java @@ -81,7 +81,6 @@ import org.springframework.test.util.ReflectionTestUtils; import com.cloud.domain.dao.DomainDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; -import com.cloud.network.Network; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; import com.cloud.user.Account; @@ -90,9 +89,6 @@ import com.cloud.user.AccountVO; import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionCallback; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.NicDetailVO; -import com.cloud.vm.NicVO; -import com.cloud.vm.VMInstanceVO; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.NicDetailsDao; import com.cloud.vm.dao.VMInstanceDao; @@ -573,32 +569,6 @@ public class DnsProviderManagerImplTest { manager.checkDnsZonePermission(callerMock, zoneVO); } - @Test - public void testDeleteDnsRecordForVMNoNicDetail() { - 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.deleteDnsRecordForVM(vm, network, nic); - verify(dnsZoneNetworkMapDao, never()).findByNetworkId(anyLong()); - } - - @Test - public void testDeleteDnsRecordForVMNicDetailBlankValue() { - Network network = mock(Network.class); - NicVO nic = mock(NicVO.class); - VMInstanceVO vm = mock(VMInstanceVO.class); - NicDetailVO detail = mock(NicDetailVO.class); - when(nic.getId()).thenReturn(50L); - when(vm.getInstanceName()).thenReturn("vm-1"); - when(nicDetailsDao.findDetail(50L, "nicdnsrecord")).thenReturn(detail); - when(detail.getValue()).thenReturn(" "); - manager.deleteDnsRecordForVM(vm, network, nic); - verify(dnsZoneNetworkMapDao, never()).findByNetworkId(anyLong()); - } - @Test public void testGetCommandsReturnsNonEmptyList() { List> commands = manager.getCommands(); @@ -720,31 +690,6 @@ public class DnsProviderManagerImplTest { verify(dnsProviderMock).deleteRecord(any(), any(), any()); } - @Test - public void testDeleteDnsRecordForVMSuccess() throws Exception { - Network network = mock(Network.class); - NicVO nic = mock(NicVO.class); - when(nic.getIPv4Address()).thenReturn("1.2.3.4"); - VMInstanceVO vm = mock(VMInstanceVO.class); - NicDetailVO detail = mock(NicDetailVO.class); - when(nic.getId()).thenReturn(50L); - when(vm.getInstanceName()).thenReturn("vm-1"); - when(nicDetailsDao.findDetail(50L, "nicdnsrecord")).thenReturn(detail); - when(detail.getValue()).thenReturn("vm-1.ex.com"); - - DnsZoneNetworkMapVO mapping = mock(DnsZoneNetworkMapVO.class); - when(network.getId()).thenReturn(NETWORK_ID); - when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(mapping); - when(mapping.getDnsZoneId()).thenReturn(ZONE_ID); - when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); - when(dnsServerDao.findById(anyLong())).thenReturn(serverVO); - when(dnsProviderMock.deleteRecord(any(), any(), any())).thenReturn("vm-1.ex.com"); - - manager.deleteDnsRecordForVM(vm, network, nic); - verify(dnsProviderMock).deleteRecord(any(), any(), any()); - verify(nicDetailsDao).removeDetail(50L, "nicdnsrecord"); - } - @Test public void testConfigure() throws Exception { assertTrue(manager.configure("dnsProviderManagerImpl", Collections.emptyMap()));