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 afc7407c150..9b664393cf5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1354,6 +1354,7 @@ public class ApiConstants { public static final String CREDENTIALS = "credentials"; public static final String DNS_ZONE_ID = "dnszoneid"; public static final String DNS_ZONE = "dnszone"; + public static final String DNS_RECORD = "dnsrecord"; public static final String DNS_SUB_DOMAIN = "dnssubdomain"; public static final String DNS_SERVER_ID = "dnsserverid"; public static final String CONTENT = "content"; 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 75e96a0fb28..1d441b01931 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -26,7 +26,7 @@ import com.cloud.utils.component.Adapter; public interface DnsProvider extends Adapter { interface Topics { - String DNS_RECORD_DELETE = "dns.record.delete"; + String DNS_RECORD_LIFECYCLE = "dns.record.lifecycle"; } DnsProviderType getProviderType(); @@ -45,4 +45,5 @@ public interface DnsProvider extends Adapter { List listRecords(DnsServer server, DnsZone zone) throws DnsProviderException; String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; String deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; + boolean dnsRecordExists(DnsServer server, DnsZone zone, String recordName, String recordType) throws DnsProviderException; } diff --git a/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java index 8e756d224fc..a1b5d88b3a5 100644 --- a/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java +++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java @@ -240,6 +240,26 @@ public class PowerDnsClient implements AutoCloseable { return rrsets.isArray() ? rrsets : Collections.emptyList(); } + public boolean dnsRecordExists(String baseUrl, Integer port, String apiKey, + String externalServerId, String zoneName, + String recordName, String type) throws DnsProviderException { + + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + String normalizedRecord = normalizeRecordName(recordName, normalizedZone); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + String urlPath = "/servers/" + externalServerId + "/zones/" + encodedZone + + "?rrset_name=" + URLEncoder.encode(normalizedRecord, StandardCharsets.UTF_8) + + "&rrset_type=" + type.toUpperCase(); + HttpGet request = new HttpGet(buildUrl(baseUrl, port, urlPath)); + JsonNode zoneNode = execute(request, apiKey, 200); + if (zoneNode == null || !zoneNode.has(ApiConstants.RR_SETS)) { + return false; + } + JsonNode rrsets = zoneNode.path(ApiConstants.RR_SETS); + return rrsets.isArray() && !rrsets.isEmpty(); + } + private JsonNode execute(HttpUriRequest request, String apiKey, int... expectedStatus) throws DnsProviderException { request.addHeader(ApiConstants.X_API_KEY, apiKey); request.addHeader("Accept", "application/json"); diff --git a/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java index 897488e5a57..d5dedfc4408 100644 --- a/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java +++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java @@ -153,6 +153,11 @@ public class PowerDnsProvider extends AdapterBase implements DnsProvider { return records; } + public boolean dnsRecordExists(DnsServer server, DnsZone zone, String recordName, String recordType) throws DnsProviderException { + return client.dnsRecordExists(server.getUrl(), server.getPort(), server.getApiKey(), + server.getExternalServerId(), zone.getName(), recordName, recordType); + } + void validateRequiredServerAndZoneFields(DnsServer server, DnsZone zone) { validateRequiredServerFields(server); if (StringUtils.isBlank(zone.getName())) { 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 a67b518a260..34635d7e957 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -17,15 +17,19 @@ 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 java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; 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; @@ -69,6 +73,7 @@ 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; @@ -457,6 +462,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa 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); @@ -474,15 +480,14 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa Account caller = CallContext.current().getCallingAccount(); accountMgr.checkAccess(caller, null, true, zone); DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); + DnsRecord.RecordType recordType = cmd.getType(); try { DnsRecord record = new DnsRecord(); record.setName(cmd.getName()); - record.setType(cmd.getType()); + record.setType(recordType); DnsProvider provider = getProviderByType(server.getProviderType()); String deletedDnsRecord = provider.deleteRecord(server, zone, record); - if (deletedDnsRecord != null) { - messageBus.publish(_name, DnsProvider.Topics.DNS_RECORD_DELETE, PublishScope.GLOBAL, deletedDnsRecord); - } + publishDnsRecordEventMessageBus(deletedDnsRecord, recordType, caller.getAccountId(), EVENT_DNS_RECORD_DELETE, null); return deletedDnsRecord != null; } catch (Exception ex) { logger.error("Failed to delete DNS record via provider", ex); @@ -713,11 +718,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa logger.warn("DNS server is not found to process DNS record for Instance: {}", instance.getInstanceName()); return; } - // Construct FQDN Prefix (e.g., "instance-id.dnsZoneName" or "instance-id.subdomain.dnsZoneName") - String recordName = String.valueOf(instance.getInstanceName()); - if (StringUtils.isNotBlank(dnsZoneNetworkMap.getSubDomain())) { - recordName = String.join(".", recordName, dnsZoneNetworkMap.getSubDomain(), dnsZone.getName()); - } + 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()); @@ -726,6 +727,31 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa 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(); @@ -836,14 +862,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa public boolean configure(String name, Map params) throws ConfigurationException { messageBus.subscribe(VirtualMachineManager.Topics.VM_LIFECYCLE, new VmLifecycleSubscriber()); messageBus.subscribe(Nic.Topics.NIC_LIFECYCLE, new NicLifecycleSubscriber()); - messageBus.subscribe(DnsProvider.Topics.DNS_RECORD_DELETE, (senderAddress, subject, args) -> { - try { - String deletedDnsRecord = (String) args; - nicDetailsDao.removeDetailsForValuesIn(ApiConstants.NIC_DNS_RECORD, Collections.singletonList(deletedDnsRecord)); - } catch (Exception ex) { - logger.error("Failed to process DNS record deletion event", ex); - } - }); + messageBus.subscribe(DnsProvider.Topics.DNS_RECORD_LIFECYCLE, new DnsRecordLifecycleSubscriber()); return true; } @@ -944,6 +963,36 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa } } + class DnsRecordLifecycleSubscriber implements MessageSubscriber { + @Override + public void onPublishMessage(String senderAddress, String subject, Object args) { + try { + logger.trace("DNS record lifecycle 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)); + } + } catch (Exception ex) { + logger.error("Failed to process DNS record lifecycle event", ex); + } + } + } + private void handleNicEvent(long nicId, long instanceId, boolean isAddDnsRecord) { VMInstanceVO vmInstanceVO = vmInstanceDao.findById(instanceId); if (vmInstanceVO == null) { @@ -986,4 +1035,25 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa deleteDnsRecordForVM(vmInstanceVO, network, nic); } } + + 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); + } + } } diff --git a/ui/src/views/network/dns/DnsRecordsTab.vue b/ui/src/views/network/dns/DnsRecordsTab.vue index a227d15a011..3f7f7187ea4 100644 --- a/ui/src/views/network/dns/DnsRecordsTab.vue +++ b/ui/src/views/network/dns/DnsRecordsTab.vue @@ -187,9 +187,7 @@ export default { const params = { dnszoneid: this.resource.id, name: record.name, - type: record.type, - contents: record.contents.join(','), - ttl: record.ttl + type: record.type } console.log('DeleteDnsRecord params', params)