From 7b9fc0e48ebd4ae893bf4e2c2ab22d64534e49ed Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 4 Feb 2026 23:04:56 +0530 Subject: [PATCH 01/23] wip: dns provider framework --- .../main/java/com/cloud/event/EventTypes.java | 20 +++ .../org/apache/cloudstack/api/BaseCmd.java | 4 + .../api/command/user/dns/AddDnsServerCmd.java | 79 +++++++++++ .../command/user/dns/CreateDnsRecordCmd.java | 60 ++++++++ .../command/user/dns/CreateDnsZoneCmd.java | 134 ++++++++++++++++++ .../command/user/dns/DeleteDnsRecordCmd.java | 57 ++++++++ .../command/user/dns/DeleteDnsServerCmd.java | 80 +++++++++++ .../command/user/dns/DeleteDnsZoneCmd.java | 84 +++++++++++ .../command/user/dns/ListDnsProvidersCmd.java | 53 +++++++ .../command/user/dns/ListDnsRecordsCmd.java | 30 ++++ .../command/user/dns/ListDnsServersCmd.java | 61 ++++++++ .../api/command/user/dns/ListDnsZonesCmd.java | 49 +++++++ .../api/response/DnsProviderResponse.java | 48 +++++++ .../api/response/DnsRecordResponse.java | 71 ++++++++++ .../api/response/DnsServerResponse.java | 67 +++++++++ .../api/response/DnsZoneResponse.java | 62 ++++++++ .../apache/cloudstack/dns/DnsProvider.java | 39 +++++ .../cloudstack/dns/DnsProviderManager.java | 67 +++++++++ .../org/apache/cloudstack/dns/DnsRecord.java | 64 +++++++++ .../org/apache/cloudstack/dns/DnsServer.java | 39 +++++ .../org/apache/cloudstack/dns/DnsZone.java | 41 ++++++ tools/apidoc/gen_toc.py | 3 +- 22 files changed, 1211 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java create mode 100644 api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java create mode 100644 api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java create mode 100644 api/src/main/java/org/apache/cloudstack/dns/DnsServer.java create mode 100644 api/src/main/java/org/apache/cloudstack/dns/DnsZone.java diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 889e821a0905..e4ac32d06ee9 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -30,6 +30,9 @@ import org.apache.cloudstack.backup.BackupRepositoryService; import org.apache.cloudstack.config.Configuration; import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet; +import org.apache.cloudstack.dns.DnsRecord; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.DnsZone; import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.extension.ExtensionCustomAction; import org.apache.cloudstack.gpu.GpuCard; @@ -859,6 +862,14 @@ public class EventTypes { public static final String EVENT_BACKUP_REPOSITORY_ADD = "BACKUP.REPOSITORY.ADD"; public static final String EVENT_BACKUP_REPOSITORY_UPDATE = "BACKUP.REPOSITORY.UPDATE"; + // DNS Framework Events + public static final String EVENT_DNS_SERVER_ADD = "DNS.SERVER.ADD"; + public static final String EVENT_DNS_SERVER_DELETE = "DNS.SERVER.DELETE"; + public static final String EVENT_DNS_ZONE_CREATE = "DNS.ZONE.CREATE"; + 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"; + static { // TODO: need a way to force author adding event types to declare the entity details as well, with out braking @@ -1397,6 +1408,15 @@ public class EventTypes { // Backup Repository entityEventDetails.put(EVENT_BACKUP_REPOSITORY_ADD, BackupRepositoryService.class); entityEventDetails.put(EVENT_BACKUP_REPOSITORY_UPDATE, BackupRepositoryService.class); + + // DNS Framework Events + entityEventDetails.put(EVENT_DNS_SERVER_ADD, DnsServer.class); + entityEventDetails.put(EVENT_DNS_SERVER_DELETE, DnsServer.class); + entityEventDetails.put(EVENT_DNS_ZONE_CREATE, DnsZone.class); + entityEventDetails.put(EVENT_DNS_ZONE_DELETE, DnsZone.class); + entityEventDetails.put(EVENT_DNS_RECORD_CREATE, DnsRecord.class); + entityEventDetails.put(EVENT_DNS_RECORD_DELETE, DnsRecord.class); + } public static boolean isNetworkEvent(String eventType) { diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java index f30f6f327826..9847d6072a44 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java @@ -40,6 +40,7 @@ import org.apache.cloudstack.alert.AlertService; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.gpu.GpuService; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.network.lb.ApplicationLoadBalancerService; @@ -230,6 +231,9 @@ public static enum CommandType { @Inject public RoutedIpv4Manager routedIpv4Manager; + @Inject + public DnsProviderManager dnsProviderManager; + public abstract void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java new file mode 100644 index 000000000000..aa1218dd357c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.user.dns; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderManager; +import org.apache.cloudstack.dns.DnsServer; + +@APICommand(name = "addDnsServer", description = "Adds a new external DNS server", + responseObject = DnsServerResponse.class, requestHasSensitiveInfo = true) +public class AddDnsServerCmd extends BaseCmd { + + @Inject + DnsProviderManager dnsProviderManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + /// + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "Name of the DNS server") + private String name; + + @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "API URL of the provider") + private String url; + + @Parameter(name = "provider", type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") + private String provider; + + @Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING, description = "API Username") + private String username; + + @Parameter(name = ApiConstants.PASSWORD, type = CommandType.STRING, description = "API Password or Token") + private String password; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { return name; } + public String getUrl() { return url; } + public String getProvider() { return provider; } + public String getUsername() { return username; } + public String getPassword() { return password; } + + @Override + public void execute() { + DnsServer server = dnsProviderManager.addDnsServer(this); + DnsServerResponse response = dnsProviderManager.createDnsServerResponse(server); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java new file mode 100644 index 000000000000..b041c46b72eb --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java @@ -0,0 +1,60 @@ +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; + +@APICommand(name = "createDnsRecord", description = "Creates a DNS record directly on the provider", + responseObject = DnsRecordResponse.class) +public class CreateDnsRecordCmd extends BaseAsyncCmd { + + @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true) + private Long zoneId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "Record name") + private String name; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "Record type (A, CNAME)") + private String type; + + @Parameter(name = "content", type = CommandType.STRING, required = true, description = "IP or target") + private String content; + + @Parameter(name = "ttl", type = CommandType.INTEGER, description = "Time to live") + private Integer ttl; + + // Getters + public Long getZoneId() { return zoneId; } + public String getName() { return name; } + public String getType() { return type; } + public String getContent() { return content; } + public Integer getTtl() { return (ttl == null) ? 3600 : ttl; } + + @Override + public void execute() { + try { + DnsRecordResponse response = dnsProviderManager.createDnsRecord(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create DNS Record: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + + @Override + public String getEventType() { return EventTypes.EVENT_DNS_RECORD_CREATE; } + + @Override + public String getEventDescription() { return "Creating DNS Record: " + getName(); } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java new file mode 100644 index 000000000000..7c1e2f9c7f53 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java @@ -0,0 +1,134 @@ +package org.apache.cloudstack.api.command.user.dns; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCreateCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.NetworkResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderManager; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ResourceAllocationException; + +@APICommand(name = "createDnsZone", description = "Creates a new DNS Zone on a specific server", + responseObject = DnsZoneResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { + + private static final String s_name = "creatednszoneresponse"; + + @Inject + DnsProviderManager dnsProviderManager; + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, + description = "The name of the DNS zone (e.g. example.com)") + private String name; + + @Parameter(name = "dnsserverid", type = CommandType.UUID, entityType = DnsServerResponse.class, + required = true, description = "The ID of the DNS server to host this zone") + private Long dnsServerId; + + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, + description = "Optional: The Guest Network to associate with this zone for auto-registration") + private Long networkId; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, + description = "The type of zone (Public, Private). Defaults to Public.") + private String type; + + // Standard CloudStack ownership parameters (account/domain) are handled + // automatically by the BaseCmd parent if we access them via getEntityOwnerId() + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + public Long getDnsServerId() { + return dnsServerId; + } + + public Long getNetworkId() { + return networkId; + } + + public String getType() { + return type; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void create() throws ResourceAllocationException { + // Phase 1: DB Persist + // The manager should create the DnsZoneVO in 'Allocating' state + try { + DnsZone zone = dnsProviderManager.allocDnsZone(this); + if (zone != null) { + setEntityId(zone.getId()); + setEntityUuid(zone.getUuid()); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create DNS Zone entity"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to allocate DNS Zone: " + e.getMessage()); + } + } + + @Override + public void execute() { + // Phase 2: Action (Call Plugin) + // The manager should retrieve the zone by ID, call the plugin, and update state to 'Ready' + try { + // Note: We use getEntityId() which was set in the create() phase + DnsZone result = dnsProviderManager.provisionDnsZone(getEntityId()); + + if (result != null) { + DnsZoneResponse response = dnsProviderManager.createDnsZoneResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to provision DNS Zone on external provider"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to provision DNS Zone: " + e.getMessage()); + } + } + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_DNS_ZONE_CREATE; // You must add this constant to EventTypes.java + } + + @Override + public String getEventDescription() { + return "creating DNS zone: " + getName(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java new file mode 100644 index 000000000000..3b0ea4a73eec --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java @@ -0,0 +1,57 @@ +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; + +@APICommand(name = "deleteDnsRecord", description = "Deletes a DNS record from the external provider", + responseObject = SuccessResponse.class) +public class DeleteDnsRecordCmd extends BaseAsyncCmd { + + @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "The ID of the DNS zone") + private Long zoneId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true) + private String name; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true) + private String type; + + // Getters + public Long getZoneId() { return zoneId; } + public String getName() { return name; } + public String getType() { return type; } + + @Override + public void execute() { + try { + boolean result = dnsProviderManager.deleteDnsRecord(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS Record"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Error deleting DNS Record: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + + @Override + public String getEventType() { return EventTypes.EVENT_DNS_RECORD_DELETE; } + + @Override + public String getEventDescription() { return "Deleting DNS Record: " + getName(); } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java new file mode 100644 index 000000000000..6328c7397f87 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java @@ -0,0 +1,80 @@ +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.dns.DnsServer; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; + +@APICommand(name = "deleteDnsServer", description = "Removes a DNS server integration", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class DeleteDnsServerCmd extends BaseAsyncCmd { + private static final String COMMAND_RESPONSE_NAME = "deletednsserverresponse"; + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, + required = true, description = "the ID of the DNS server") + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + try { + boolean result = dnsProviderManager.deleteDnsServer(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS server"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS server: " + e.getMessage()); + } + } + + @Override + public String getCommandName() { + return COMMAND_RESPONSE_NAME; + } + + @Override + public long getEntityOwnerId() { + // Look up the server to find its owner. + // This allows the Framework to check: Is Caller == Owner? + DnsServer server = dnsProviderManager.getDnsServer(id); + if (server != null) { + return server.getAccountId(); + } + + // If server not found, return System to fail safely (or let manager handle 404) + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public String getEventType() { return EventTypes.EVENT_DNS_SERVER_DELETE; } + + @Override + public String getEventDescription() { return "Deleting DNS Server ID: " + getId(); } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java new file mode 100644 index 000000000000..b15357620c0f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java @@ -0,0 +1,84 @@ +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; + +@APICommand(name = "deleteDnsZone", description = "Removes a DNS Zone from CloudStack and the external provider", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class DeleteDnsZoneCmd extends BaseAsyncCmd { + private static final String COMMAND_RESPONSE_NAME = "deletednszoneresponse"; + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "the ID of the DNS zone") + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + try { + // The Manager handles both DB removal and Plugin execution + boolean result = dnsProviderManager.deleteDnsZone(this); + + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS Zone"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS Zone: " + e.getMessage()); + } + } + + @Override + public String getCommandName() { + return COMMAND_RESPONSE_NAME; + } + + @Override + public long getEntityOwnerId() { + // Look up the Zone to find the Account Owner + DnsZone zone = dnsProviderManager.getDnsZone(id); + if (zone != null) { + return zone.getAccountId(); + } + // Fallback or System if not found (likely to fail in execute() anyway) + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_DNS_ZONE_DELETE; // Ensure this constant is added to EventTypes + } + + @Override + public String getEventDescription() { + return "Deleting DNS Zone ID: " + getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java new file mode 100644 index 000000000000..1be4d068e1de --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.user.dns; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.response.DnsProviderResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsProviderManager; + +@APICommand(name = "listDnsProviders", description = "Lists available DNS plugin providers", + responseObject = DnsProviderResponse.class, requestHasSensitiveInfo = false) +public class ListDnsProvidersCmd extends BaseListCmd { + + @Inject + DnsProviderManager dnsManager; + + @Override + public void execute() { + List providers = dnsManager.listProviderNames(); + ListResponse response = new ListResponse<>(); + List responses = new ArrayList<>(); + for (String name : providers) { + DnsProviderResponse resp = new DnsProviderResponse(); + resp.setName(name); + resp.setObjectName("dnsprovider"); + responses.add(resp); + } + response.setResponses(responses); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java new file mode 100644 index 000000000000..a0c401250eda --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java @@ -0,0 +1,30 @@ +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; + +@APICommand(name = "listDnsRecords", description = "Lists DNS records from the external provider", + responseObject = DnsRecordResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class ListDnsRecordsCmd extends BaseListCmd { + @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "the ID of the DNS zone to list records from") + private Long zoneId; + + public Long getZoneId() { + return zoneId; + } + + @Override + public void execute() { + // The manager will fetch live data from the plugin + ListResponse response = dnsProviderManager.listDnsRecords(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java new file mode 100644 index 000000000000..5a2a4a8778f1 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java @@ -0,0 +1,61 @@ +package org.apache.cloudstack.api.command.user.dns; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListAccountResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsProviderManager; + +@APICommand(name = "listDnsServers", description = "Lists DNS servers owned by the account.", + responseObject = DnsServerResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class ListDnsServersCmd extends BaseListAccountResourcesCmd { + private static final String COMMAND_RESPONSE_NAME = "listdnsserversresponse"; + + @Inject + DnsProviderManager dnsProviderManager; + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, + description = "the ID of the DNS server") + private Long id; + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, + description = "filter by provider type (e.g. PowerDNS, Cloudflare)") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + ListResponse response = dnsProviderManager.listDnsServers(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return COMMAND_RESPONSE_NAME; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java new file mode 100644 index 000000000000..0d7750c339c6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java @@ -0,0 +1,49 @@ +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListAccountResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.NetworkResponse; + +@APICommand(name = "listDnsZones", description = "Lists DNS Zones.", + responseObject = DnsZoneResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class ListDnsZonesCmd extends BaseListAccountResourcesCmd { + private static final String COMMAND_RESPONSE_NAME = "listdnszonesresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + /// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + description = "list DNS zone by ID") + private Long id; + + @Parameter(name = "dnsserverid", type = CommandType.UUID, entityType = DnsServerResponse.class, + description = "list DNS zones belonging to a specific DNS Server") + private Long dnsServerId; + + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, + description = "list DNS zones associated with a specific Network") + private Long networkId; + + public Long getId() { return id; } + public Long getDnsServerId() { return dnsServerId; } + public Long getNetworkId() { return networkId; } + + @Override + public void execute() { + ListResponse response = dnsProviderManager.listDnsZones(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return COMMAND_RESPONSE_NAME; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java new file mode 100644 index 000000000000..f4e892ac85cd --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java @@ -0,0 +1,48 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class DnsProviderResponse extends BaseResponse { + + @SerializedName(ApiConstants.NAME) + @Param(description = "The name of the DNS provider (e.g. PowerDNS, Cloudflare)") + private String name; + + // Constructors + public DnsProviderResponse() {} + + public DnsProviderResponse(String name) { + this.name = name; + setObjectName("dnsprovider"); // Sets the JSON wrapper name + } + + // Accessors + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java new file mode 100644 index 000000000000..578a0f940723 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.dns.DnsRecord; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class DnsRecordResponse extends BaseResponse { + + @SerializedName(ApiConstants.NAME) + @Param(description = "the name of the DNS record") + private String name; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "the type of the DNS record (A, CNAME, etc)") + private String type; + + @SerializedName("content") + @Param(description = "the content of the record (IP address or target)") + private String content; + + @SerializedName("ttl") + @Param(description = "the time to live (TTL) in seconds") + private Integer ttl; + + @SerializedName(ApiConstants.ZONE_ID) + @Param(description = "the ID of the zone this record belongs to") + private String zoneId; + + @SerializedName("sourceid") + @Param(description = "the external ID of the record on the provider") + private String sourceId; + + public DnsRecordResponse() { + super(); + setObjectName("dnsrecord"); + } + + // Setters + public void setName(String name) { this.name = name; } + + // Accepts String or Enum.toString() + public void setType(String type) { this.type = type; } + public void setType(DnsRecord.RecordType type) { + this.type = (type != null) ? type.name() : null; + } + + public void setContent(String content) { this.content = content; } + public void setTtl(Integer ttl) { this.ttl = ttl; } + public void setZoneId(String zoneId) { this.zoneId = zoneId; } + public void setSourceId(String sourceId) { this.sourceId = sourceId; } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java new file mode 100644 index 000000000000..393542cb2578 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java @@ -0,0 +1,67 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.dns.DnsServer; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = DnsServer.class) +public class DnsServerResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "the ID of the DNS server") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "the name of the DNS server") + private String name; + + @SerializedName(ApiConstants.URL) + @Param(description = "the URL of the DNS server API") + private String url; + + @SerializedName(ApiConstants.PROVIDER) + @Param(description = "the provider type of the DNS server") + private String provider; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "the account associated with the DNS server") + private String accountName; + + @SerializedName(ApiConstants.PROJECT) + @Param(description = "the project name of the DNS server") + private String projectName; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "the domain ID of the DNS server") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "the domain name of the DNS server") + private String domainName; + + public DnsServerResponse() { + super(); + setObjectName("dnsserver"); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java new file mode 100644 index 000000000000..9519555e2b78 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java @@ -0,0 +1,62 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = DnsZone.class) +public class DnsZoneResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "the ID of the DNS zone") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "the name of the DNS zone") + private String name; + + @SerializedName("dnsserverid") + @Param(description = "the ID of the DNS server this zone belongs to") + private String dnsServerId; + + @SerializedName("dnsservername") + @Param(description = "the name of the DNS server this zone belongs to") + private String dnsServerName; + + @SerializedName(ApiConstants.NETWORK_ID) + @Param(description = "the ID of the network this zone is associated with") + private String networkId; + + @SerializedName(ApiConstants.NETWORK_NAME) + @Param(description = "the name of the network this zone is associated with") + private String networkName; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "the type of the zone (Public/Private)") + private String type; + + public DnsZoneResponse() { + super(); + setObjectName("dnszone"); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java new file mode 100644 index 000000000000..9b558b007efa --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -0,0 +1,39 @@ +// 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; + +import java.util.List; + +public interface DnsProvider { + // Returns the provider type string (e.g., "PowerDNS") + String getProviderType(); + + // Validates connectivity to the server + boolean validate(DnsServer server); + + // Zone Operations + boolean createZone(DnsServer server, DnsZone zone); + boolean deleteZone(DnsServer server, DnsZone zone); + + + DnsRecord createRecord(DnsServer server, DnsZone zone, DnsRecord record); + boolean updateRecord(DnsServer server, DnsZone zone, DnsRecord record); + boolean deleteRecord(DnsServer server, DnsZone zone, DnsRecord record); + + List listRecords(DnsServer server, DnsZone zone); +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java new file mode 100644 index 000000000000..cd232d511b15 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -0,0 +1,67 @@ +// 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; + +import java.util.List; + +import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd; +import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsRecordCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsZoneCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; + +import com.cloud.utils.component.Manager; + +public interface DnsProviderManager extends Manager { + DnsServer addDnsServer(AddDnsServerCmd cmd); + ListResponse listDnsServers(ListDnsServersCmd cmd); + boolean deleteDnsServer(DeleteDnsServerCmd cmd); + DnsServerResponse createDnsServerResponse(DnsServer server); + + DnsServer getDnsServer(Long id); + + DnsZone createDnsZone(CreateDnsZoneCmd cmd); + boolean deleteDnsZone(DeleteDnsZoneCmd cmd); + ListResponse listDnsZones(ListDnsZonesCmd cmd); + + DnsZone getDnsZone(long id); + + DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd); + boolean deleteDnsRecord(DeleteDnsRecordCmd cmd); + + ListResponse listDnsRecords(ListDnsRecordsCmd cmd); + + List listProviderNames(); + + // Allocates the DB row (State: Allocating) + DnsZone allocDnsZone(CreateDnsZoneCmd cmd); + + // Calls the Plugin (State: Allocating -> Ready/Error) + DnsZone provisionDnsZone(long zoneId); + + // Helper to create the response object + DnsZoneResponse createDnsZoneResponse(DnsZone zone); +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java b/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java new file mode 100644 index 000000000000..7ee6b51c3f2d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java @@ -0,0 +1,64 @@ +// 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; + +import com.cloud.utils.exception.CloudRuntimeException; + +public class DnsRecord { + + public enum RecordType { + A, AAAA, CNAME, MX, TXT, SRV, PTR, NS; + + public static RecordType fromString(String type) { + if (type == null) return null; + try { + return RecordType.valueOf(type.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new CloudRuntimeException("Invalid DNS Record Type: " + type + + ". Supported: " + java.util.Arrays.toString(values())); + } + } + } + + private String name; + private RecordType type; // Enforced Enum here + private String content; + private int ttl; + + public DnsRecord() {} + + public DnsRecord(String name, RecordType type, String content, int ttl) { + this.name = name; + this.type = type; + this.content = content; + this.ttl = ttl; + } + + // Getters and Setters + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public RecordType getType() { return type; } + public void setType(RecordType type) { this.type = type; } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + + public int getTtl() { return ttl; } + public void setTtl(int ttl) { this.ttl = ttl; } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java new file mode 100644 index 000000000000..616399f870ac --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java @@ -0,0 +1,39 @@ +// 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; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface DnsServer extends InternalIdentity, Identity { + enum ProviderType { + PowerDNS + } + + String getName(); + + String getUrl(); + + ProviderType getProviderType(); + + String getKey(); + + long getDomainId(); + + long getAccountId(); +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java new file mode 100644 index 000000000000..0aa5a819dcbb --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java @@ -0,0 +1,41 @@ +// 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; + +import java.util.List; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface DnsZone extends InternalIdentity, Identity { + enum ZoneType { + Public, Private + } + + String getName(); + + long getDnsServerId(); + + long getAccountId(); + + String getDescription(); + + ZoneType getType(); + + List getAssociatedNetworks(); +} diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index e41a04ff2e1b..ee7624250f6f 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -273,7 +273,8 @@ 'Extensions' : 'Extension', 'CustomAction' : 'Extension', 'CustomActions' : 'Extension', - 'ImportVmTask': 'Import VM Task' + 'ImportVmTask': 'Import VM Task', + 'Dns': 'DNS' } From b2d597c1bb23c86af2281dc25a4b2acab450b924 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 10 Feb 2026 07:24:10 +0530 Subject: [PATCH 02/23] add db schema, vo and dao classes --- .../apache/cloudstack/dns/DnsProvider.java | 7 +- .../cloudstack/dns/DnsProviderManager.java | 4 +- .../cloudstack/dns/DnsProviderType.java | 34 ++++ .../org/apache/cloudstack/dns/DnsServer.java | 20 +- .../org/apache/cloudstack/dns/DnsZone.java | 5 +- .../META-INF/db/schema-42210to42300.sql | 51 +++++ .../dns/DnsProviderManagerImpl.java | 177 ++++++++++++++++++ .../cloudstack/dns/dao/DnsServerDao.java | 30 +++ .../cloudstack/dns/dao/DnsServerDaoImpl.java | 47 +++++ .../apache/cloudstack/dns/dao/DnsZoneDao.java | 28 +++ .../cloudstack/dns/dao/DnsZoneDaoImpl.java | 47 +++++ .../dns/dao/DnsZoneNetworkMapDao.java | 11 ++ .../dns/dao/DnsZoneNetworkMapDaoImpl.java | 28 +++ .../apache/cloudstack/dns/vo/DnsServerVO.java | 157 ++++++++++++++++ .../dns/vo/DnsZoneNetworkMapVO.java | 96 ++++++++++ .../apache/cloudstack/dns/vo/DnsZoneVO.java | 125 +++++++++++++ .../spring-server-core-managers-context.xml | 6 + 17 files changed, 860 insertions(+), 13 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java 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 9b558b007efa..38c155fc72aa 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -19,9 +19,10 @@ import java.util.List; -public interface DnsProvider { - // Returns the provider type string (e.g., "PowerDNS") - String getProviderType(); +import com.cloud.utils.component.Adapter; + +public interface DnsProvider extends Adapter { + DnsProviderType getProviderType(); // Validates connectivity to the server boolean validate(DnsServer 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 cd232d511b15..0703c559f619 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -34,8 +34,10 @@ import org.apache.cloudstack.api.response.ListResponse; import com.cloud.utils.component.Manager; +import com.cloud.utils.component.PluggableService; + +public interface DnsProviderManager extends Manager, PluggableService { -public interface DnsProviderManager extends Manager { DnsServer addDnsServer(AddDnsServerCmd cmd); ListResponse listDnsServers(ListDnsServersCmd cmd); boolean deleteDnsServer(DeleteDnsServerCmd cmd); diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java new file mode 100644 index 000000000000..0b8f88d67450 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java @@ -0,0 +1,34 @@ +// 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; + +public enum DnsProviderType { + PowerDNS; +// Cloudflare + + // Helper to validate and return Enum from String safely + public static DnsProviderType fromString(String type) { + if (type == null) return null; + for (DnsProviderType t : DnsProviderType.values()) { + if (t.name().equalsIgnoreCase(type)) { + return t; + } + } + throw new IllegalArgumentException("Invalid DNS Provider type: " + type); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java index 616399f870ac..b399a9747012 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java @@ -17,23 +17,29 @@ package org.apache.cloudstack.dns; +import java.util.Date; + import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; public interface DnsServer extends InternalIdentity, Identity { - enum ProviderType { - PowerDNS - } + enum State { + Enabled, Disabled; + }; String getName(); String getUrl(); - ProviderType getProviderType(); - - String getKey(); + DnsProviderType getProviderType(); - long getDomainId(); + String getAPIKey(); long getAccountId(); + + boolean isPublic(); + + Date getCreated(); + + Date getRemoved(); } diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java index 0aa5a819dcbb..1e8e8010689c 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java @@ -26,6 +26,9 @@ public interface DnsZone extends InternalIdentity, Identity { enum ZoneType { Public, Private } + enum State { + Active, Inactive + } String getName(); @@ -33,8 +36,6 @@ enum ZoneType { long getAccountId(); - String getDescription(); - ZoneType getType(); List getAssociatedNetworks(); 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 d330ecd0c0d5..f549ec441450 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 @@ -49,3 +49,54 @@ CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` ( INDEX `i_webhook_filter__webhook_id`(`webhook_id`), CONSTRAINT `fk_webhook_filter__webhook_id` FOREIGN KEY(`webhook_id`) REFERENCES `webhook`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +-- ====================================================================== +-- DNS Framework Schema +-- ====================================================================== + +-- 1. DNS Server Table (Stores DNS Server Configurations) +CREATE TABLE `cloud`.`dns_server` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns server', + `uuid` varchar(255) COMMENT 'uuid of the dns server', + `name` varchar(255) NOT NULL COMMENT 'display name of the dns server', + `url` varchar(1024) NOT NULL COMMENT 'dns server url', + `port` int(11) DEFAULT NULL COMMENT 'optional dns server port', + `is_public` tinyint(1) NOT NULL DEFAULT '0', + `public_domain_suffix` VARCHAR(255), + `state` ENUM('Enabled', 'Disabled') NOT NULL DEFAULT 'Disabled', + `account_id` bigint(20) unsigned NOT NULL, + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', + PRIMARY KEY (`id`), + KEY `i_dns_server__account_id` (`account_id`), + CONSTRAINT `fk_dns_server__account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. DNS Zone Table (Stores DNS Zone Metadata) +CREATE TABLE `cloud`.`dns_zone` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone', + `uuid` varchar(255) COMMENT 'uuid of the dns zone', + `name` varchar(255) NOT NULL COMMENT 'dns zone name (e.g. example.com)', + `dns_server_id` bigint unsigned NOT NULL COMMENT 'fk to dns_server.id', + `external_reference` VARCHAR(255) COMMENT 'id of external provider resource', + `state` ENUM('Active', 'Inactive') NOT NULL DEFAULT 'Inactive', + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', + PRIMARY KEY (`id`), + KEY `i_dns_zone__dns_server` (`dns_server_id`), + CONSTRAINT `fk_dns_zone__dns_server_id` FOREIGN KEY (`dns_server_id`) REFERENCES `dns_server` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. DNS Zone Network Map (One-to-Many Link) +CREATE TABLE `cloud`.`dns_zone_network_map` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone to network mapping', + `dns_zone_id` bigint(20) unsigned NOT NULL, + `network_id` bigint(20) unsigned NOT NULL COMMENT 'network to which dns zone is associated to', + `sub_domain` varchar(255) DEFAULT NULL COMMENT 'Subdomain for auto-registration', + PRIMARY KEY (`id`), + KEY `fk_dns_map__zone_id` (`dns_zone_id`), + KEY `fk_dns_map__network_id` (`network_id`), + CONSTRAINT `fk_dns_map__zone_id` FOREIGN KEY (`dns_zone_id`) REFERENCES `dns_zone` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dns_map__network_id` FOREIGN KEY (`network_id`) REFERENCES `networks` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java new file mode 100644 index 000000000000..09293f2c8591 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -0,0 +1,177 @@ +// 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; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd; +import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsRecordCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsZoneCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsProvidersCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.component.PluggableService; +import com.cloud.utils.exception.CloudRuntimeException; + +@Component +public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderManager, PluggableService { + @Autowired(required = false) + List dnsProviders; + + private DnsProvider getProvider(DnsProviderType type) { + if (type == null) { + throw new CloudRuntimeException("Provider type cannot be null"); + } + for (DnsProvider provider : dnsProviders) { + if (provider.getProviderType() == type) { + return provider; + } + } + throw new CloudRuntimeException("No plugin found for DNS Provider type: " + type); + } + + @Override + public DnsServer addDnsServer(AddDnsServerCmd cmd) { + return null; + } + + @Override + public ListResponse listDnsServers(ListDnsServersCmd cmd) { + return null; + } + + @Override + public boolean deleteDnsServer(DeleteDnsServerCmd cmd) { + return false; + } + + @Override + public DnsServerResponse createDnsServerResponse(DnsServer server) { + return null; + } + + @Override + public DnsServer getDnsServer(Long id) { + return null; + } + + @Override + public DnsZone createDnsZone(CreateDnsZoneCmd cmd) { + return null; + } + + @Override + public boolean deleteDnsZone(DeleteDnsZoneCmd cmd) { + return false; + } + + @Override + public ListResponse listDnsZones(ListDnsZonesCmd cmd) { + return null; + } + + @Override + public DnsZone getDnsZone(long id) { + return null; + } + + @Override + public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { + return null; + } + + @Override + public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) { + return false; + } + + @Override + public ListResponse listDnsRecords(ListDnsRecordsCmd cmd) { + return null; + } + + @Override + public List listProviderNames() { + List providerNames = new ArrayList<>(); + if (dnsProviders != null) { + for (DnsProvider provider : dnsProviders) { + providerNames.add(provider.getProviderType().toString()); + } + } + return providerNames; + } + + @Override + public DnsZone allocDnsZone(CreateDnsZoneCmd cmd) { + return null; + } + + @Override + public DnsZone provisionDnsZone(long zoneId) { + return null; + } + + @Override + public DnsZoneResponse createDnsZoneResponse(DnsZone zone) { + return null; + } + + @Override + public boolean start() { + if (dnsProviders == null || dnsProviders.isEmpty()) { + logger.warn("DNS Framework started but no provider plugins were found!"); + } else { + logger.info("DNS Framework started with: {} providers.", dnsProviders.size()); + } + return true; + } + + @Override + public List> getCommands() { + List> cmdList = new ArrayList<>(); + // DNS Server Commands + cmdList.add(AddDnsServerCmd.class); + cmdList.add(ListDnsServersCmd.class); + cmdList.add(DeleteDnsServerCmd.class); + cmdList.add(ListDnsProvidersCmd.class); + + // DNS Zone Commands + cmdList.add(CreateDnsZoneCmd.class); + cmdList.add(ListDnsZonesCmd.class); + cmdList.add(DeleteDnsZoneCmd.class); + + // DNS Record Commands + cmdList.add(CreateDnsRecordCmd.class); + cmdList.add(ListDnsRecordsCmd.class); + cmdList.add(DeleteDnsRecordCmd.class); + return cmdList; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java new file mode 100644 index 000000000000..5fada010f62d --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java @@ -0,0 +1,30 @@ +// 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.DnsServerVO; + +import com.cloud.utils.db.GenericDao; + +public interface DnsServerDao extends GenericDao { + + List listByProviderType(String providerType); + +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java new file mode 100644 index 000000000000..8967aa25d521 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java @@ -0,0 +1,47 @@ +// 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.DnsServerVO; +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class DnsServerDaoImpl extends GenericDaoBase implements DnsServerDao { + static final String PROVIDER_TYPE = "providerType"; + SearchBuilder ProviderSearch; + + public DnsServerDaoImpl() { + super(); + ProviderSearch = createSearchBuilder(); + ProviderSearch.and(PROVIDER_TYPE, ProviderSearch.entity().getProviderType(), SearchCriteria.Op.EQ); + ProviderSearch.done(); + } + + @Override + public List listByProviderType(String providerType) { + SearchCriteria sc = ProviderSearch.create(); + sc.setParameters(PROVIDER_TYPE, providerType); + return listBy(sc); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java new file mode 100644 index 000000000000..30d341021439 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java @@ -0,0 +1,28 @@ +// 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.DnsZoneVO; + +import com.cloud.utils.db.GenericDao; + +public interface DnsZoneDao extends GenericDao { + List listByServerId(long serverId); +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java new file mode 100644 index 000000000000..1a573e4132ae --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java @@ -0,0 +1,47 @@ +// 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.DnsZoneVO; +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class DnsZoneDaoImpl extends GenericDaoBase implements DnsZoneDao { + static final String DNS_SERVER_ID = "dnsServerId"; + SearchBuilder ServerSearch; + + public DnsZoneDaoImpl() { + super(); + ServerSearch = createSearchBuilder(); + ServerSearch.and(DNS_SERVER_ID, ServerSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); + ServerSearch.done(); + } + + @Override + public List listByServerId(long serverId) { + SearchCriteria sc = ServerSearch.create(); + sc.setParameters(DNS_SERVER_ID, serverId); + return listBy(sc); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java new file mode 100644 index 000000000000..9d79a0e8708d --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java @@ -0,0 +1,11 @@ +package org.apache.cloudstack.dns.dao; + +import java.util.List; + +import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; + +import com.cloud.utils.db.GenericDao; + +public interface DnsZoneNetworkMapDao extends GenericDao { + List listByDnsZoneId(long dnsZoneId); +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java new file mode 100644 index 000000000000..adf1a09111f0 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java @@ -0,0 +1,28 @@ +package org.apache.cloudstack.dns.dao; + +import java.util.List; + +import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class DnsZoneNetworkMapDaoImpl extends GenericDaoBase implements DnsZoneNetworkMapDao { + final SearchBuilder ZoneSearch; + + public DnsZoneNetworkMapDaoImpl() { + super(); + ZoneSearch = createSearchBuilder(); + ZoneSearch.and("dnsZoneId", ZoneSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ); + ZoneSearch.done(); + } + @Override + public List listByDnsZoneId(long dnsZoneId) { + SearchCriteria sc = ZoneSearch.create(); + sc.setParameters("dnsZoneId", dnsZoneId); + return listBy(sc); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java new file mode 100644 index 000000000000..0b80f474a528 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java @@ -0,0 +1,157 @@ +// 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 java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.DnsServer; + +import com.cloud.utils.db.Encrypt; +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "dns_server") +public class DnsServerVO implements DnsServer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "url") + private String url; + + @Column(name = "provider_type") + @Enumerated(EnumType.STRING) + private DnsProviderType providerType; + + @Encrypt + @Column(name = "api_key") + private String apiKey; + + @Column(name = "is_public") + private boolean isPublic; + + @Column(name = "public_domain_suffix") + private String publicDomainSuffix; + + @Column(name = "state") + @Enumerated(EnumType.STRING) + private State state; + + @Column(name = "account_id") + private long accountId; + + @Column(name = GenericDao.CREATED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date created = null; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed = null; + + public DnsServerVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + } + + public DnsServerVO(String name, String url, DnsProviderType providerType, String apiKey, long accountId, boolean isPublic) { + this(); + this.name = name; + this.url = url; + this.providerType = providerType; + this.apiKey = apiKey; + this.accountId = accountId; + this.isPublic = isPublic; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getUrl() { + return url; + } + + @Override + public DnsProviderType getProviderType() { + return providerType; + } + + @Override + public String getAPIKey() { + return apiKey; + } + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public Date getCreated() { + return created; + } + + @Override + public Date getRemoved() { + return removed; + } + + @Override + public String getUuid() { + return uuid; + } + + public boolean isPublic() { + return isPublic; + } + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java new file mode 100644 index 000000000000..9500240d81a1 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java @@ -0,0 +1,96 @@ +// 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 javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.api.InternalIdentity; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "dns_zone_network_map") +public class DnsZoneNetworkMapVO implements InternalIdentity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "dns_zone_id") + private long dnsZoneId; + + @Column(name = "network_id") + private long networkId; + + @Column(name = "sub_domain") + private String subDomain; + + @Column(name = GenericDao.CREATED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date created = null; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed = null; + + public DnsZoneNetworkMapVO() { + this.created = new Date(); + } + + public DnsZoneNetworkMapVO(long dnsZoneId, long networkId, String subDomain) { + this(); + this.dnsZoneId = dnsZoneId; + this.networkId = networkId; + this.subDomain = subDomain; + } + + @Override + public long getId() { + return id; + } + + public long getDnsZoneId() { + return dnsZoneId; + } + + public long getNetworkId() { + return networkId; + } + + public String getSubDomain() { + return subDomain; + } + + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java new file mode 100644 index 000000000000..4495081cd209 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java @@ -0,0 +1,125 @@ +// 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 java.util.List; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "dns_zone") +public class DnsZoneVO implements DnsZone { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "dns_server_id") + private long dnsServerId; + + @Column(name = "external_reference") + private String externalReference; + + @Column(name = "type") + @Enumerated(EnumType.STRING) + private ZoneType type; + + @Column(name = "state") + @Enumerated(EnumType.STRING) + private State state; + + @Column(name = "account_id") + private long accountId; + + @Column(name = GenericDao.CREATED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date created = null; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed = null; + + public DnsZoneVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + } + + public DnsZoneVO(String name, long dnsServerId, long accountId) { + this(); + this.name = name; + this.dnsServerId = dnsServerId; + this.accountId = accountId; + this.type = ZoneType.Public; + } + + @Override + public String getName() { + return name; + } + + @Override + public long getDnsServerId() { + return dnsServerId; + } + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public ZoneType getType() { + return type; + } + + @Override + public List getAssociatedNetworks() { + return List.of(); + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public long getId() { + return id; + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index b90c40dc95e7..3cec97379045 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -398,4 +398,10 @@ + + + + + + From 6ef7f9bcd4833cc7826151526111b222ea5d99a9 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 11 Feb 2026 11:19:07 +0530 Subject: [PATCH 03/23] add powerdns module in plugins --- client/pom.xml | 6 ++ .../spring-core-registry-core-context.xml | 2 + .../META-INF/cloudstack/dns/module.properties | 21 ++++++ ...core-lifecycle-dns-context-inheritable.xml | 31 ++++++++ plugins/dns/powerdns/pom.xml | 37 ++++++++++ .../dns/powerdns/PowerDnsProvider.java | 70 +++++++++++++++++++ .../cloudstack/powerdns/module.properties | 18 +++++ .../powerdns/spring-dns-powerdns-context.xml | 31 ++++++++ plugins/pom.xml | 1 + .../dns/DnsProviderManagerImpl.java | 10 ++- .../spring-server-core-managers-context.xml | 4 +- 11 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 core/src/main/resources/META-INF/cloudstack/dns/module.properties create mode 100644 core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml create mode 100644 plugins/dns/powerdns/pom.xml create mode 100644 plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java create mode 100644 plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/module.properties create mode 100644 plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml diff --git a/client/pom.xml b/client/pom.xml index b8dffe65d4fb..5c7abe771dce 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -662,6 +662,12 @@ cloud-utils ${project.version} + + org.apache.cloudstack + cloud-plugin-dns-powerdns + ${project.version} + + diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index 01c568d78916..8bca42f16bc0 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -366,4 +366,6 @@ + + diff --git a/core/src/main/resources/META-INF/cloudstack/dns/module.properties b/core/src/main/resources/META-INF/cloudstack/dns/module.properties new file mode 100644 index 000000000000..a2bb467be751 --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/dns/module.properties @@ -0,0 +1,21 @@ +# +# 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. +# + +name=dns +parent=core diff --git a/core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml new file mode 100644 index 000000000000..27cac9400284 --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/plugins/dns/powerdns/pom.xml b/plugins/dns/powerdns/pom.xml new file mode 100644 index 000000000000..ef915559f478 --- /dev/null +++ b/plugins/dns/powerdns/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + cloud-plugin-dns-powerdns + Apache CloudStack Plugin - PowerDNS + + org.apache.cloudstack + cloudstack-plugins + 4.23.0.0-SNAPSHOT + ../../pom.xml + + + + 11 + 11 + UTF-8 + + + 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 new file mode 100644 index 000000000000..c18820e4134f --- /dev/null +++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java @@ -0,0 +1,70 @@ +// 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.powerdns; + +import java.util.List; + +import org.apache.cloudstack.dns.DnsProvider; +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.DnsRecord; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.utils.component.AdapterBase; + +public class PowerDnsProvider extends AdapterBase implements DnsProvider { + @Override + public DnsProviderType getProviderType() { + return DnsProviderType.PowerDNS; + } + + @Override + public boolean validate(DnsServer server) { + return false; + } + + @Override + public boolean createZone(DnsServer server, DnsZone zone) { + return false; + } + + @Override + public boolean deleteZone(DnsServer server, DnsZone zone) { + return false; + } + + @Override + public DnsRecord createRecord(DnsServer server, DnsZone zone, DnsRecord record) { + return null; + } + + @Override + public boolean updateRecord(DnsServer server, DnsZone zone, DnsRecord record) { + return false; + } + + @Override + public boolean deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) { + return false; + } + + @Override + public List listRecords(DnsServer server, DnsZone zone) { + return List.of(); + } +} diff --git a/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/module.properties b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/module.properties new file mode 100644 index 000000000000..baec3fde6a6a --- /dev/null +++ b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/module.properties @@ -0,0 +1,18 @@ +# 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. +name=powerdns +parent=dns diff --git a/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml new file mode 100644 index 000000000000..c9e4937dac55 --- /dev/null +++ b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml @@ -0,0 +1,31 @@ + + + + + diff --git a/plugins/pom.xml b/plugins/pom.xml index e7d13871285e..acd5efad7de2 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -154,6 +154,7 @@ user-two-factor-authenticators/totp user-two-factor-authenticators/static-pin + dns/powerdns 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 09293f2c8591..93d3c42d0ac4 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -34,7 +34,6 @@ import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.cloud.utils.component.ManagerBase; @@ -43,7 +42,6 @@ @Component public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderManager, PluggableService { - @Autowired(required = false) List dnsProviders; private DnsProvider getProvider(DnsProviderType type) { @@ -174,4 +172,12 @@ public List> getCommands() { cmdList.add(DeleteDnsRecordCmd.class); return cmdList; } + + public List getDnsProviders() { + return dnsProviders; + } + + public void setDnsProviders(List dnsProviders) { + this.dnsProviders = dnsProviders; + } } diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 3cec97379045..02b38e386344 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -403,5 +403,7 @@ - + + + From 9911c280e177398455dce3bc49922d7663cc7159 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Thu, 12 Feb 2026 19:27:32 +0530 Subject: [PATCH 04/23] Changes done to AddDnsServer, ListDnsServer, DeleteDnsServer and UpdateDnsServer --- .../apache/cloudstack/api/ApiConstants.java | 5 + .../api/command/user/dns/AddDnsServerCmd.java | 64 +++++-- .../command/user/dns/DeleteDnsServerCmd.java | 2 +- .../command/user/dns/ListDnsServersCmd.java | 1 + .../command/user/dns/UpdateDnsServerCmd.java | 112 +++++++++++++ .../api/response/DnsServerResponse.java | 71 +++++--- .../apache/cloudstack/dns/DnsProvider.java | 2 +- .../cloudstack/dns/DnsProviderManager.java | 2 + .../org/apache/cloudstack/dns/DnsServer.java | 2 +- .../META-INF/db/schema-42210to42300.sql | 3 + .../dns/powerdns/PowerDnsClient.java | 118 +++++++++++++ .../dns/powerdns/PowerDnsProvider.java | 32 +++- .../dns/DnsProviderManagerImpl.java | 156 +++++++++++++++++- .../cloudstack/dns/dao/DnsServerDao.java | 8 +- .../cloudstack/dns/dao/DnsServerDaoImpl.java | 53 +++++- .../apache/cloudstack/dns/vo/DnsServerVO.java | 56 ++++++- 16 files changed, 637 insertions(+), 50 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java create mode 100644 plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java 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 1ab6fba6081f..a64e40fa7a2a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1333,6 +1333,11 @@ public class ApiConstants { public static final String OBJECT_STORAGE_LIMIT = "objectstoragelimit"; public static final String OBJECT_STORAGE_TOTAL = "objectstoragetotal"; + // DNS provider related + public static final String NAME_SERVERS = "nameservers"; + public static final String CREDENTIALS = "credentials"; + public static final String PUBLIC_DOMAIN_SUFFIX = "publicdomainsuffix"; + public static final String PARAMETER_DESCRIPTION_ACTIVATION_RULE = "Quota tariff's activation rule. It can receive a JS script that results in either " + "a boolean or a numeric value: if it results in a boolean value, the tariff value will be applied according to the result; if it results in a numeric value, the " + "numeric value will be applied; if the result is neither a boolean nor a numeric value, the tariff will not be applied. If the rule is not informed, the tariff " + diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java index aa1218dd357c..764992d6e811 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -21,12 +21,15 @@ import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.dns.DnsServer; +import org.apache.commons.lang3.BooleanUtils; @APICommand(name = "addDnsServer", description = "Adds a new external DNS server", responseObject = DnsServerResponse.class, requestHasSensitiveInfo = true) @@ -45,14 +48,23 @@ public class AddDnsServerCmd extends BaseCmd { @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "API URL of the provider") private String url; - @Parameter(name = "provider", type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") private String provider; - @Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING, description = "API Username") - private String username; + @Parameter(name = ApiConstants.CREDENTIALS, type = CommandType.STRING, required = false, description = "API Key or Credentials for the external provider") + private String credentials; - @Parameter(name = ApiConstants.PASSWORD, type = CommandType.STRING, description = "API Password or Token") - private String password; + @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") + private Integer port; + + @Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, description = "Whether the DNS server is publicly accessible by other accounts") + private Boolean isPublic; + + @Parameter(name = ApiConstants.PUBLIC_DOMAIN_SUFFIX, type = CommandType.STRING, description = "The domain suffix used for public access (e.g. public.example.com)") + private String publicDomainSuffix; + + @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.STRING, description = "Comma separated list of name servers") + private String nameServers; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -61,19 +73,45 @@ public class AddDnsServerCmd extends BaseCmd { public String getName() { return name; } public String getUrl() { return url; } public String getProvider() { return provider; } - public String getUsername() { return username; } - public String getPassword() { return password; } + public String getCredentials() { + return credentials; + } - @Override - public void execute() { - DnsServer server = dnsProviderManager.addDnsServer(this); - DnsServerResponse response = dnsProviderManager.createDnsServerResponse(server); - response.setResponseName(getCommandName()); - setResponseObject(response); + public Integer getPort() { + return port; + } + + public Boolean isPublic() { + return BooleanUtils.isTrue(isPublic); + } + + public String getPublicDomainSuffix() { + return publicDomainSuffix; + } + + public String getNameServers() { + return nameServers; } @Override public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + + @Override + public void execute() { + try { + DnsServer server = dnsProviderManager.addDnsServer(this); + if (server != null) { + DnsServerResponse response = dnsProviderManager.createDnsServerResponse(server); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add DNS server"); + } + } catch (Exception ex) { + logger.error("Failed to add DNS server", ex); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java index 6328c7397f87..155aa2006317 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java @@ -76,5 +76,5 @@ public long getEntityOwnerId() { public String getEventType() { return EventTypes.EVENT_DNS_SERVER_DELETE; } @Override - public String getEventDescription() { return "Deleting DNS Server ID: " + getId(); } + public String getEventDescription() { return "Deleting DNS server ID: " + getId(); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java index 5a2a4a8778f1..cd408dcb6adb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java @@ -51,6 +51,7 @@ public String getProvider() { public void execute() { ListResponse response = dnsProviderManager.listDnsServers(this); response.setResponseName(getCommandName()); + response.setObjectName("dnsserver"); setResponseObject(response); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java new file mode 100644 index 000000000000..c9f63b7f06a1 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java @@ -0,0 +1,112 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.user.dns; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderManager; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.commons.lang3.BooleanUtils; + +@APICommand(name = "updateDnsServer", description = "Update DNS server", + responseObject = DnsServerResponse.class, requestHasSensitiveInfo = true) +public class UpdateDnsServerCmd extends BaseCmd { + + @Inject + DnsProviderManager dnsProviderManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, + required = true, description = "The ID of the DNS server to update") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Name of the DNS server") + private String name; + + @Parameter(name = ApiConstants.URL, type = CommandType.STRING, description = "API URL of the provider") + private String url; + + @Parameter(name = ApiConstants.CREDENTIALS, type = CommandType.STRING, required = false, description = "API Key or Credentials for the external provider") + private String credentials; + + @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") + private Integer port; + + @Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, description = "Whether the DNS server is publicly accessible by other accounts") + private Boolean isPublic; + + @Parameter(name = ApiConstants.PUBLIC_DOMAIN_SUFFIX, type = CommandType.STRING, description = "The domain suffix used for public access (e.g. public.example.com)") + private String publicDomainSuffix; + + @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.STRING, description = "Comma separated list of name servers") + private String nameServers; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { return id; } + public String getName() { return name; } + public String getUrl() { return url; } + public String getCredentials() { + return credentials; + } + public Integer getPort() { + return port; + } + public Boolean isPublic() { + return BooleanUtils.isTrue(isPublic); + } + public String getPublicDomainSuffix() { + return publicDomainSuffix; + } + public String getNameServers() { return nameServers; } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public void execute() { + try { + DnsServer server = dnsProviderManager.updateDnsServer(this); + if (server != null) { + DnsServerResponse response = dnsProviderManager.createDnsServerResponse(server); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update DNS server"); + } + } catch (Exception ex) { + logger.error("Failed to add update server", ex); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java index 393542cb2578..57f6ce28d8f8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java @@ -17,9 +17,12 @@ package org.apache.cloudstack.api.response; +import java.util.List; + import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.dns.DnsProviderType; import org.apache.cloudstack.dns.DnsServer; import com.cloud.serializer.Param; @@ -29,39 +32,69 @@ public class DnsServerResponse extends BaseResponse { @SerializedName(ApiConstants.ID) - @Param(description = "the ID of the DNS server") + @Param(description = "ID of the DNS server") private String id; @SerializedName(ApiConstants.NAME) - @Param(description = "the name of the DNS server") + @Param(description = "Name of the DNS server") private String name; @SerializedName(ApiConstants.URL) - @Param(description = "the URL of the DNS server API") + @Param(description = "URL of the DNS server API") private String url; - @SerializedName(ApiConstants.PROVIDER) - @Param(description = "the provider type of the DNS server") - private String provider; + @SerializedName(ApiConstants.PORT) + @Param(description = "The port of the DNS server") + private Integer port; - @SerializedName(ApiConstants.ACCOUNT) - @Param(description = "the account associated with the DNS server") - private String accountName; + @SerializedName(ApiConstants.PROVIDER) + @Param(description = "The provider type of the DNS server") + private DnsProviderType provider; - @SerializedName(ApiConstants.PROJECT) - @Param(description = "the project name of the DNS server") - private String projectName; + @SerializedName(ApiConstants.IS_PUBLIC) + @Param(description = "Is the DNS server publicly available") + private Boolean isPublic; - @SerializedName(ApiConstants.DOMAIN_ID) - @Param(description = "the domain ID of the DNS server") - private String domainId; + @SerializedName(ApiConstants.PUBLIC_DOMAIN_SUFFIX) @Param(description = "The public domain suffix for the DNS server") + private String publicDomainSuffix; - @SerializedName(ApiConstants.DOMAIN) - @Param(description = "the domain name of the DNS server") - private String domainName; + @SerializedName(ApiConstants.NAME_SERVERS) @Param(description = "Name servers entries associated to DNS server") + private List nameServers; public DnsServerResponse() { super(); - setObjectName("dnsserver"); + + } + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setProvider(DnsProviderType provider) { + this.provider = provider; + } + + public void setPublic(Boolean value) { + isPublic = value; + } + + public void setPort(Integer port) { + this.port = port; + } + + public void setPublicDomainSuffix(String publicDomainSuffix) { + this.publicDomainSuffix = publicDomainSuffix; + } + + public void setNameServers(List nameServers) { + this.nameServers = nameServers; } } 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 38c155fc72aa..c06875713757 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -25,7 +25,7 @@ public interface DnsProvider extends Adapter { DnsProviderType getProviderType(); // Validates connectivity to the server - boolean validate(DnsServer server); + boolean validate(DnsServer server) throws Exception; // Zone Operations boolean createZone(DnsServer server, DnsZone zone); 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 0703c559f619..138b32943dcf 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -28,6 +28,7 @@ import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; +import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; @@ -40,6 +41,7 @@ public interface DnsProviderManager extends Manager, PluggableService { DnsServer addDnsServer(AddDnsServerCmd cmd); ListResponse listDnsServers(ListDnsServersCmd cmd); + DnsServer updateDnsServer(UpdateDnsServerCmd cmd); boolean deleteDnsServer(DeleteDnsServerCmd cmd); DnsServerResponse createDnsServerResponse(DnsServer server); diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java index b399a9747012..dafd40017e57 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java @@ -33,7 +33,7 @@ enum State { DnsProviderType getProviderType(); - String getAPIKey(); + String getApiKey(); long getAccountId(); 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 f549ec441450..d58dc3a56bbc 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 @@ -60,8 +60,11 @@ CREATE TABLE `cloud`.`dns_server` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns server', `uuid` varchar(255) COMMENT 'uuid of the dns server', `name` varchar(255) NOT NULL COMMENT 'display name of the dns server', + `provider_type` varchar(255) NOT NULL COMMENT 'Provider type such as PowerDns', `url` varchar(1024) NOT NULL COMMENT 'dns server url', + `api_key` varchar(255) NOT NULL COMMENT 'dns server api_key', `port` int(11) DEFAULT NULL COMMENT 'optional dns server port', + `name_servers` varchar(1024) DEFAULT NULL COMMENT 'Comma separated list of name servers', `is_public` tinyint(1) NOT NULL DEFAULT '0', `public_domain_suffix` VARCHAR(255), `state` ENUM('Enabled', 'Disabled') NOT NULL DEFAULT 'Disabled', 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 new file mode 100644 index 000000000000..1377519202ee --- /dev/null +++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java @@ -0,0 +1,118 @@ +// 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.powerdns; + +import java.io.IOException; + +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.cloud.utils.exception.CloudRuntimeException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class PowerDnsClient implements AutoCloseable { + public static final Logger logger = LoggerFactory.getLogger(PowerDnsClient.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final int TIMEOUT_MS = 5000; + private final CloseableHttpClient httpClient; + + public void validate(String baseUrl, String apiKey) { + String normalizedUrl = baseUrl.trim(); + + if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) { + normalizedUrl = "http://" + normalizedUrl; // default to HTTP + } + + if (normalizedUrl.endsWith("/")) { + normalizedUrl = normalizedUrl.substring(0, normalizedUrl.length() - 1); + } + + + String checkUrl = normalizedUrl + "/api/v1/servers"; + + HttpGet request = new HttpGet(checkUrl); + request.addHeader("X-API-Key", apiKey); + request.addHeader("Accept", "application/json"); + + try (CloseableHttpResponse response = httpClient.execute(request)) { + int statusCode = response.getStatusLine().getStatusCode(); + String body = response.getEntity() != null + ? EntityUtils.toString(response.getEntity()) + : null; + + if (statusCode == HttpStatus.SC_OK) { + JsonNode root = MAPPER.readTree(body); + + if (!root.isArray() || root.isEmpty()) { + throw new CloudRuntimeException("No servers returned by PowerDNS API"); + } + + boolean authoritativeFound = false; + for (JsonNode node : root) { + if ("authoritative".equalsIgnoreCase(node.path("daemon_type").asText(null))) { + authoritativeFound = true; + break; + } + } + + if (!authoritativeFound) { + throw new CloudRuntimeException("No authoritative PowerDNS server found"); + } + + } else if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { + throw new CloudRuntimeException("Invalid PowerDNS API key"); + } else { + logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body); + throw new CloudRuntimeException(String.format("PowerDNS validation failed with HTTP %d", statusCode)); + } + + } catch (IOException ex) { + throw new CloudRuntimeException("Failed to connect to PowerDNS", ex); + } + } + + public PowerDnsClient() { + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(TIMEOUT_MS) + .setConnectionRequestTimeout(TIMEOUT_MS) + .setSocketTimeout(TIMEOUT_MS) + .build(); + + this.httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(config) + .disableCookieManagement() + .build(); + } + + @Override + public void close() { + try { + httpClient.close(); + } catch (IOException e) { + logger.warn("Failed to close PowerDNS HTTP client", e); + } + } +} 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 c18820e4134f..ba774745d056 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 @@ -18,6 +18,7 @@ package org.apache.cloudstack.dns.powerdns; import java.util.List; +import java.util.Map; import org.apache.cloudstack.dns.DnsProvider; import org.apache.cloudstack.dns.DnsProviderType; @@ -25,17 +26,28 @@ import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.dns.DnsZone; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.AdapterBase; public class PowerDnsProvider extends AdapterBase implements DnsProvider { + + private PowerDnsClient client; + @Override public DnsProviderType getProviderType() { return DnsProviderType.PowerDNS; } - @Override public boolean validate(DnsServer server) { - return false; + if (StringUtils.isBlank(server.getUrl())) { + throw new IllegalArgumentException("PowerDNS API URL cannot be empty"); + } + if (StringUtils.isBlank(server.getApiKey())) { + throw new IllegalArgumentException("PowerDNS API key cannot be empty"); + } + client.validate(server.getUrl(), server.getApiKey()); + logger.debug("PowerDNS credentials validated for {}", server.getUrl()); + return true; } @Override @@ -67,4 +79,20 @@ public boolean deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) { public List listRecords(DnsServer server, DnsZone zone) { return List.of(); } + + @Override + public boolean configure(String name, Map params) { + if (client == null) { + client = new PowerDnsClient(); + } + return true; + } + + @Override + public boolean stop() { + if (client != null) { + client.close(); + } + return true; + } } 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 93d3c42d0ac4..4d5c0e5d1cae 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; import org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd; import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; @@ -30,19 +32,33 @@ import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; +import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; import org.apache.cloudstack.api.response.DnsServerResponse; 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.DnsServerDao; +import org.apache.cloudstack.dns.vo.DnsServerVO; import org.springframework.stereotype.Component; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; +import com.cloud.utils.db.Filter; import com.cloud.utils.exception.CloudRuntimeException; @Component public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderManager, PluggableService { List dnsProviders; + @Inject + AccountManager accountMgr; + @Inject + DnsServerDao dnsServerDao; private DnsProvider getProvider(DnsProviderType type) { if (type == null) { @@ -58,22 +74,150 @@ private DnsProvider getProvider(DnsProviderType type) { @Override public DnsServer addDnsServer(AddDnsServerCmd cmd) { - return null; + Account caller = CallContext.current().getCallingAccount(); + DnsServer existing = dnsServerDao.findByUrlAndAccount(cmd.getUrl(), caller.getId()); + if (existing != null) { + throw new InvalidParameterValueException( + "This Account already has a DNS Server integration for URL: " + cmd.getUrl()); + } + + DnsProviderType type = DnsProviderType.fromString(cmd.getProvider()); + DnsProvider provider = getProvider(type); + DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), type, cmd.getCredentials(), cmd.getPort(), + cmd.isPublic(), cmd.getPublicDomainSuffix(), cmd.getNameServers(), caller.getId()); + try { + provider.validate(server); + } catch (Exception ex) { + logger.error("Failed to validate DNS server", ex); + throw new CloudRuntimeException("Failed to validate DNS server"); + } + return dnsServerDao.persist(server); } @Override public ListResponse listDnsServers(ListDnsServersCmd cmd) { - return null; + Account caller = CallContext.current().getCallingAccount(); + Long accountIdFilter = null; + if (accountMgr.isRootAdmin(caller.getId())) { + // Root Admin: Can see all, unless they specifically ask for an account + if (cmd.getAccountName() != null) { + Account target = accountMgr.getActiveAccountByName(cmd.getAccountName(), cmd.getDomainId()); + if (target == null) { + return new ListResponse<>(); // Account not found + } + accountIdFilter = target.getId(); + } + } else { + // Regular User / Domain Admin: STRICTLY restricted to their own account + accountIdFilter = caller.getId(); + } + + Filter searchFilter = new Filter(DnsServerVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + Pair, Integer> result = dnsServerDao.searchDnsServers(cmd.getId(), cmd.getKeyword(), + cmd.getProvider(), accountIdFilter, searchFilter); + + ListResponse response = new ListResponse<>(); + List serverResponses = new ArrayList<>(); + for (DnsServerVO server : result.first()) { + serverResponses.add(createDnsServerResponse(server)); + } + response.setResponses(serverResponses, result.second()); + return response; + } + + @Override + public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { + Long dnsServerId = cmd.getId(); + DnsServerVO dnsServer = dnsServerDao.findById(dnsServerId); + if (dnsServer == null) { + throw new InvalidParameterValueException(String.format("DNS Server with ID: %s not found.", dnsServerId)); + } + + Account caller = CallContext.current().getCallingAccount(); + if (!accountMgr.isRootAdmin(caller.getId()) && dnsServer.getAccountId() != caller.getId()) { + throw new PermissionDeniedException("You do not have permission to update this DNS server."); + } + + boolean validationRequired = false; + String originalUrl = dnsServer.getUrl(); + String originalKey = dnsServer.getApiKey(); + + if (cmd.getName() != null) { + dnsServer.setName(cmd.getName()); + } + + if (cmd.getUrl() != null) { + if (!cmd.getUrl().equals(originalUrl)) { + DnsServer duplicate = dnsServerDao.findByUrlAndAccount(cmd.getUrl(), dnsServer.getAccountId()); + if (duplicate != null && duplicate.getId() != dnsServer.getId()) { + throw new InvalidParameterValueException("Another DNS Server with this URL already exists."); + } + dnsServer.setUrl(cmd.getUrl()); + validationRequired = true; + } + } + + if (cmd.getCredentials() != null && !cmd.getCredentials().equals(originalKey)) { + dnsServer.setApiKey(cmd.getCredentials()); + validationRequired = true; + } + + if (cmd.getPort() != null) { + dnsServer.setPort(cmd.getPort()); + } + if (cmd.isPublic() != null) { + dnsServer.setIsPublic(cmd.isPublic()); + } + + if (cmd.getPublicDomainSuffix() != null) { + dnsServer.setPublicDomainSuffix(cmd.getPublicDomainSuffix()); + } + + if (cmd.getNameServers() != null) { + dnsServer.setNameServers(cmd.getNameServers()); + } + if (validationRequired) { + DnsProvider provider = getProvider(dnsServer.getProviderType()); + try { + provider.validate(dnsServer); + } catch (Exception ex) { + logger.error("Validation failed for DNS server", ex); + throw new InvalidParameterValueException("Validation failed for DNS server"); + } + } + boolean updateStatus = dnsServerDao.update(dnsServerId, dnsServer); + + if (updateStatus) { + return dnsServerDao.findById(dnsServerId); + } else { + throw new CloudRuntimeException(String.format("Unable to update DNS server: %s", dnsServer.getName())); + } } @Override public boolean deleteDnsServer(DeleteDnsServerCmd cmd) { - return false; + Long dnsServerId = cmd.getId(); + DnsServerVO dnsServer = dnsServerDao.findById(dnsServerId); + if (dnsServer == null) { + throw new InvalidParameterValueException(String.format("DNS Server with ID: %s not found.", dnsServerId)); + } + Account caller = CallContext.current().getCallingAccount(); + if (!accountMgr.isRootAdmin(caller.getId()) && dnsServer.getAccountId() != caller.getId()) { + throw new PermissionDeniedException("You do not have permission to delete this DNS server."); + } + return dnsServerDao.remove(dnsServerId); } @Override public DnsServerResponse createDnsServerResponse(DnsServer server) { - return null; + DnsServerResponse response = new DnsServerResponse(); + response.setId(server.getUuid()); + response.setName(server.getName()); + response.setUrl(server.getUrl()); + response.setProvider(server.getProviderType()); + response.setPublic(server.isPublic()); + response.setObjectName("dnsserver"); + return response; } @Override @@ -155,11 +299,13 @@ public boolean start() { @Override public List> getCommands() { List> cmdList = new ArrayList<>(); + + cmdList.add(ListDnsProvidersCmd.class); // DNS Server Commands cmdList.add(AddDnsServerCmd.class); cmdList.add(ListDnsServersCmd.class); cmdList.add(DeleteDnsServerCmd.class); - cmdList.add(ListDnsProvidersCmd.class); + cmdList.add(UpdateDnsServerCmd.class); // DNS Zone Commands cmdList.add(CreateDnsZoneCmd.class); diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java index 5fada010f62d..0c64f742b634 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java @@ -19,12 +19,18 @@ import java.util.List; +import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.dns.vo.DnsServerVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; public interface DnsServerDao extends GenericDao { - List listByProviderType(String providerType); + List listByProvider(String provider); + DnsServer findByUrlAndAccount(String url, long accountId); + + Pair, Integer> searchDnsServers(Long id, String keyword, String provider, Long accountId, Filter filter); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java index 8967aa25d521..e9deb9bb2ca2 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java @@ -19,29 +19,74 @@ import java.util.List; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.dns.vo.DnsServerVO; import org.springframework.stereotype.Component; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @Component public class DnsServerDaoImpl extends GenericDaoBase implements DnsServerDao { - static final String PROVIDER_TYPE = "providerType"; + SearchBuilder AllFieldsSearch; SearchBuilder ProviderSearch; + SearchBuilder AccountUrlSearch; + public DnsServerDaoImpl() { super(); ProviderSearch = createSearchBuilder(); - ProviderSearch.and(PROVIDER_TYPE, ProviderSearch.entity().getProviderType(), SearchCriteria.Op.EQ); + ProviderSearch.and(ApiConstants.PROVIDER_TYPE, ProviderSearch.entity().getProviderType(), SearchCriteria.Op.EQ); ProviderSearch.done(); + + AccountUrlSearch = createSearchBuilder(); + AccountUrlSearch.and(ApiConstants.URL, AccountUrlSearch.entity().getUrl(), SearchCriteria.Op.EQ); + AccountUrlSearch.and(ApiConstants.ACCOUNT_ID, AccountUrlSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountUrlSearch.done(); + + AllFieldsSearch = createSearchBuilder(); + AllFieldsSearch.and(ApiConstants.ID, AllFieldsSearch.entity().getId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and(ApiConstants.NAME, AllFieldsSearch.entity().getName(), SearchCriteria.Op.LIKE); + AllFieldsSearch.and(ApiConstants.PROVIDER_TYPE, AllFieldsSearch.entity().getProviderType(), SearchCriteria.Op.EQ); + AllFieldsSearch.and(ApiConstants.ACCOUNT_ID, AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AllFieldsSearch.done(); + } @Override - public List listByProviderType(String providerType) { + public List listByProvider(String providerType) { SearchCriteria sc = ProviderSearch.create(); - sc.setParameters(PROVIDER_TYPE, providerType); + sc.setParameters(ApiConstants.PROVIDER_TYPE, providerType); return listBy(sc); } + + @Override + public DnsServer findByUrlAndAccount(String url, long accountId) { + SearchCriteria sc = AccountUrlSearch.create(); + sc.setParameters(ApiConstants.URL, url); + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + return findOneBy(sc); + } + + @Override + public Pair, Integer> searchDnsServers(Long id, String keyword, String provider, Long accountId, Filter filter) { + SearchCriteria sc = AllFieldsSearch.create(); + if (id != null) { + sc.setParameters(ApiConstants.ID, id); + } + if (keyword != null) { + sc.setParameters(ApiConstants.NAME, "%" + keyword + "%"); + } + if (provider != null) { + sc.setParameters(ApiConstants.PROVIDER_TYPE, provider); + } + if (accountId != null) { + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + } + return searchAndCount(sc, filter); + } } diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java index 0b80f474a528..4e216f8fac48 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java @@ -54,6 +54,9 @@ public class DnsServerVO implements DnsServer { @Column(name = "url") private String url; + @Column(name = "port") + private Integer port; + @Column(name = "provider_type") @Enumerated(EnumType.STRING) private DnsProviderType providerType; @@ -75,6 +78,9 @@ public class DnsServerVO implements DnsServer { @Column(name = "account_id") private long accountId; + @Column(name = "name_servers") + private String nameServers; + @Column(name = GenericDao.CREATED_COLUMN) @Temporal(value = TemporalType.TIMESTAMP) private Date created = null; @@ -83,19 +89,25 @@ public class DnsServerVO implements DnsServer { @Temporal(value = TemporalType.TIMESTAMP) private Date removed = null; - public DnsServerVO() { + DnsServerVO() { this.uuid = UUID.randomUUID().toString(); this.created = new Date(); } - public DnsServerVO(String name, String url, DnsProviderType providerType, String apiKey, long accountId, boolean isPublic) { + public DnsServerVO(String name, String url, DnsProviderType providerType, String apiKey, + Integer port, boolean isPublic, String publicDomainSuffix, String nameServers, + long accountId) { this(); this.name = name; this.url = url; + this.port = port; this.providerType = providerType; this.apiKey = apiKey; this.accountId = accountId; + this.publicDomainSuffix = publicDomainSuffix; + this.nameServers = nameServers; this.isPublic = isPublic; + this.state = State.Enabled; } @Override @@ -119,7 +131,7 @@ public DnsProviderType getProviderType() { } @Override - public String getAPIKey() { + public String getApiKey() { return apiKey; } @@ -154,4 +166,42 @@ public State getState() { public void setState(State state) { this.state = state; } + + @Override + public String toString() { + return "DnsServerVO {" + + "id=" + id + + ", name='" + name + '\'' + + ", url='" + url + '\'' + + ", apiKey='*****'" + + "}"; + } + + public void setNameServers(String nameServers) { + this.nameServers = nameServers; + } + + public void setIsPublic(boolean value) { + isPublic = value; + } + + public void setPublicDomainSuffix(String publicDomainSuffix) { + this.publicDomainSuffix = publicDomainSuffix; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public void setPort(Integer port) { + this.port = port; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setName(String name) { + this.name = name; + } } From df2131810f9cc4454f899bd91a6ec5caeadd2c8d Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Fri, 13 Feb 2026 13:52:08 +0530 Subject: [PATCH 05/23] 1. Setup Dns zone schema 2. added relevant changes in dao and vo 3. worked on creatednszone, integration with mgr 4. powerdns create zone api call --- .../command/user/dns/CreateDnsZoneCmd.java | 22 ++--- .../api/response/DnsZoneResponse.java | 50 ++++++++-- .../apache/cloudstack/dns/DnsProvider.java | 3 +- .../cloudstack/dns/DnsProviderManager.java | 6 +- .../org/apache/cloudstack/dns/DnsZone.java | 7 +- .../META-INF/db/schema-42210to42300.sql | 15 ++- .../dns/powerdns/PowerDnsClient.java | 93 ++++++++++++++++--- .../dns/powerdns/PowerDnsProvider.java | 16 +++- .../dns/DnsProviderManagerImpl.java | 61 +++++++++++- .../apache/cloudstack/dns/dao/DnsZoneDao.java | 3 + .../cloudstack/dns/dao/DnsZoneDaoImpl.java | 30 ++++++ .../apache/cloudstack/dns/vo/DnsZoneVO.java | 41 +++++++- 12 files changed, 293 insertions(+), 54 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java index 7c1e2f9c7f53..3e85946222c8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java @@ -23,7 +23,7 @@ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { - private static final String s_name = "creatednszoneresponse"; + private static final String COMMAND_RESPONSE_NAME = "creatednszoneresponse"; @Inject DnsProviderManager dnsProviderManager; @@ -48,8 +48,8 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { description = "The type of zone (Public, Private). Defaults to Public.") private String type; - // Standard CloudStack ownership parameters (account/domain) are handled - // automatically by the BaseCmd parent if we access them via getEntityOwnerId() + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "Display text for the zone") + private String description; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -71,16 +71,18 @@ public String getType() { return type; } + public String getDescription() { + return description; + } + ///////////////////////////////////////////////////// /////////////// Implementation ////////////////////// ///////////////////////////////////////////////////// @Override public void create() throws ResourceAllocationException { - // Phase 1: DB Persist - // The manager should create the DnsZoneVO in 'Allocating' state try { - DnsZone zone = dnsProviderManager.allocDnsZone(this); + DnsZone zone = dnsProviderManager.allocateDnsZone(this); if (zone != null) { setEntityId(zone.getId()); setEntityUuid(zone.getUuid()); @@ -94,12 +96,8 @@ public void create() throws ResourceAllocationException { @Override public void execute() { - // Phase 2: Action (Call Plugin) - // The manager should retrieve the zone by ID, call the plugin, and update state to 'Ready' try { - // Note: We use getEntityId() which was set in the create() phase DnsZone result = dnsProviderManager.provisionDnsZone(getEntityId()); - if (result != null) { DnsZoneResponse response = dnsProviderManager.createDnsZoneResponse(result); response.setResponseName(getCommandName()); @@ -114,7 +112,7 @@ public void execute() { @Override public String getCommandName() { - return s_name; + return COMMAND_RESPONSE_NAME; } @Override @@ -124,7 +122,7 @@ public long getEntityOwnerId() { @Override public String getEventType() { - return EventTypes.EVENT_DNS_ZONE_CREATE; // You must add this constant to EventTypes.java + return EventTypes.EVENT_DNS_ZONE_CREATE; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java index 9519555e2b78..f8ef89824c4d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java @@ -28,35 +28,67 @@ @EntityReference(value = DnsZone.class) public class DnsZoneResponse extends BaseResponse { @SerializedName(ApiConstants.ID) - @Param(description = "the ID of the DNS zone") + @Param(description = "ID of the DNS zone") private String id; @SerializedName(ApiConstants.NAME) - @Param(description = "the name of the DNS zone") + @Param(description = "Name of the DNS zone") private String name; @SerializedName("dnsserverid") - @Param(description = "the ID of the DNS server this zone belongs to") - private String dnsServerId; + @Param(description = "ID of the DNS server this zone belongs to") + private Long dnsServerId; @SerializedName("dnsservername") - @Param(description = "the name of the DNS server this zone belongs to") + @Param(description = "Name of the DNS server this zone belongs to") private String dnsServerName; @SerializedName(ApiConstants.NETWORK_ID) - @Param(description = "the ID of the network this zone is associated with") + @Param(description = "ID of the network this zone is associated with") private String networkId; @SerializedName(ApiConstants.NETWORK_NAME) - @Param(description = "the name of the network this zone is associated with") + @Param(description = "Name of the network this zone is associated with") private String networkName; @SerializedName(ApiConstants.TYPE) - @Param(description = "the type of the zone (Public/Private)") - private String type; + @Param(description = "The type of the zone (Public/Private)") + private DnsZone.ZoneType type; + + @SerializedName(ApiConstants.STATE) + @Param(description = "The state of the zone (Active/Inactive)") + private DnsZone.State state; public DnsZoneResponse() { super(); setObjectName("dnszone"); } + + public void setName(String name) { + this.name = name; + } + + public void setDnsServerId(Long dnsServerId) { + this.dnsServerId = dnsServerId; + } + + public void setDnsServerName(String dnsServerName) { + this.dnsServerName = dnsServerName; + } + + public void setNetworkId(String networkId) { + this.networkId = networkId; + } + + public void setNetworkName(String networkName) { + this.networkName = networkName; + } + + public void setType(DnsZone.ZoneType type) { + this.type = type; + } + + public void setState(DnsZone.State state) { + this.state = state; + } } 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 c06875713757..68ebd2a682d9 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -28,10 +28,9 @@ public interface DnsProvider extends Adapter { boolean validate(DnsServer server) throws Exception; // Zone Operations - boolean createZone(DnsServer server, DnsZone zone); + boolean provisionZone(DnsServer server, DnsZone zone) throws Exception; boolean deleteZone(DnsServer server, DnsZone zone); - DnsRecord createRecord(DnsServer server, DnsZone zone, DnsRecord record); boolean updateRecord(DnsServer server, DnsZone zone, DnsRecord record); boolean deleteRecord(DnsServer server, DnsZone zone, DnsRecord record); 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 138b32943dcf..0421e8fffb6e 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -60,10 +60,10 @@ public interface DnsProviderManager extends Manager, PluggableService { List listProviderNames(); - // Allocates the DB row (State: Allocating) - DnsZone allocDnsZone(CreateDnsZoneCmd cmd); + // Allocates the DB row (State: Inactive) + DnsZone allocateDnsZone(CreateDnsZoneCmd cmd); - // Calls the Plugin (State: Allocating -> Ready/Error) + // Calls the Plugin (State: Inactive -> Active) DnsZone provisionDnsZone(long zoneId); // Helper to create the response object diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java index 1e8e8010689c..193185a6b0c6 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java @@ -19,10 +19,11 @@ import java.util.List; +import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; -public interface DnsZone extends InternalIdentity, Identity { +public interface DnsZone extends InternalIdentity, Identity, ControlledEntity { enum ZoneType { Public, Private } @@ -38,5 +39,9 @@ enum State { ZoneType getType(); + String getDescription(); + List getAssociatedNetworks(); + + State getState(); } 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 d58dc3a56bbc..7eb199c02055 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 @@ -58,7 +58,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` ( -- 1. DNS Server Table (Stores DNS Server Configurations) CREATE TABLE `cloud`.`dns_server` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns server', - `uuid` varchar(255) COMMENT 'uuid 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', `provider_type` varchar(255) NOT NULL COMMENT 'Provider type such as PowerDns', `url` varchar(1024) NOT NULL COMMENT 'dns server url', @@ -79,16 +79,25 @@ CREATE TABLE `cloud`.`dns_server` ( -- 2. DNS Zone Table (Stores DNS Zone Metadata) CREATE TABLE `cloud`.`dns_zone` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone', - `uuid` varchar(255) COMMENT 'uuid 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)', `dns_server_id` bigint unsigned NOT NULL COMMENT 'fk to dns_server.id', `external_reference` VARCHAR(255) COMMENT 'id of external provider resource', + `domain_id` bigint unsigned COMMENT 'for domain-specific ownership', + `account_id` bigint unsigned COMMENT 'account id. foreign key to account table', + `description` varchar(1024) DEFAULT NULL, + `type` ENUM('Private', 'Public') NOT NULL DEFAULT 'Public', `state` ENUM('Active', 'Inactive') NOT NULL DEFAULT 'Inactive', `created` datetime NOT NULL COMMENT 'date created', `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', PRIMARY KEY (`id`), + CONSTRAINT `uc_dns_zone__uuid` UNIQUE (`uuid`), + CONSTRAINT `uc_dns_zone__name_server_type` UNIQUE (`name`, `dns_server_id`, `type`), KEY `i_dns_zone__dns_server` (`dns_server_id`), - CONSTRAINT `fk_dns_zone__dns_server_id` FOREIGN KEY (`dns_server_id`) REFERENCES `dns_server` (`id`) ON DELETE CASCADE + KEY `i_dns_zone__account_id` (`account_id`), + CONSTRAINT `fk_dns_zone__dns_server_id` FOREIGN KEY (`dns_server_id`) REFERENCES `dns_server` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dns_zone__account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dns_zone__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 3. DNS Zone Network Map (One-to-Many Link) 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 1377519202ee..23b23e7e7e23 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 @@ -18,11 +18,14 @@ package org.apache.cloudstack.dns.powerdns; import java.io.IOException; +import java.util.List; import org.apache.http.HttpStatus; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; @@ -32,6 +35,8 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; public class PowerDnsClient implements AutoCloseable { public static final Logger logger = LoggerFactory.getLogger(PowerDnsClient.class); @@ -40,19 +45,7 @@ public class PowerDnsClient implements AutoCloseable { private final CloseableHttpClient httpClient; public void validate(String baseUrl, String apiKey) { - String normalizedUrl = baseUrl.trim(); - - if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) { - normalizedUrl = "http://" + normalizedUrl; // default to HTTP - } - - if (normalizedUrl.endsWith("/")) { - normalizedUrl = normalizedUrl.substring(0, normalizedUrl.length() - 1); - } - - - String checkUrl = normalizedUrl + "/api/v1/servers"; - + String checkUrl = buildApiUrl(baseUrl, "/api/v1/servers"); HttpGet request = new HttpGet(checkUrl); request.addHeader("X-API-Key", apiKey); request.addHeader("Accept", "application/json"); @@ -94,6 +87,62 @@ public void validate(String baseUrl, String apiKey) { } } + public void createZone(String baseUrl, String apiKey, String zoneName, List nameservers) { + String url = buildApiUrl(baseUrl, "/servers/localhost/zones"); + ObjectNode json = MAPPER.createObjectNode(); + json.put("name", zoneName.endsWith(".") ? zoneName : zoneName + "."); + json.put("kind", "Native"); + json.put("dnssec", false); + + if (nameservers != null && !nameservers.isEmpty()) { + ArrayNode nsArray = json.putArray("nameservers"); + for (String ns : nameservers) { + nsArray.add(ns.endsWith(".") ? ns : ns + "."); + } + } + + logger.debug("Creating PowerDNS zone: {} using URL: {}", zoneName, url); + + HttpPost request = new HttpPost(url); + request.addHeader("X-API-Key", apiKey); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Accept", "application/json"); + + try { + request.setEntity(new StringEntity(json.toString())); + + try (CloseableHttpResponse response = httpClient.execute(request)) { + + int statusCode = response.getStatusLine().getStatusCode(); + String body = response.getEntity() != null + ? EntityUtils.toString(response.getEntity()) + : null; + + if (statusCode == HttpStatus.SC_CREATED) { + logger.debug("Zone {} created successfully", zoneName); + return; + } + + if (statusCode == HttpStatus.SC_CONFLICT) { + throw new CloudRuntimeException("Zone already exists: " + zoneName); + } + + if (statusCode == HttpStatus.SC_UNAUTHORIZED || + statusCode == HttpStatus.SC_FORBIDDEN) { + throw new CloudRuntimeException("Invalid PowerDNS API key"); + } + + logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body); + + throw new CloudRuntimeException(String.format("Failed to create zone %s (HTTP %d)", zoneName, statusCode)); + } + + } catch (IOException e) { + throw new CloudRuntimeException("Error while creating PowerDNS zone " + zoneName, e); + } + } + + public PowerDnsClient() { RequestConfig config = RequestConfig.custom() .setConnectTimeout(TIMEOUT_MS) @@ -107,6 +156,24 @@ public PowerDnsClient() { .build(); } + private String normalizeBaseUrl(String baseUrl) { + if (baseUrl == null) { + throw new IllegalArgumentException("PowerDNS base URL cannot be null"); + } + String normalizedUrl = baseUrl.trim(); + if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) { + normalizedUrl = "http://" + normalizedUrl; + } + if (normalizedUrl.endsWith("/")) { + normalizedUrl = normalizedUrl.substring(0, normalizedUrl.length() - 1); + } + return normalizedUrl; + } + + private String buildApiUrl(String baseUrl, String path) { + return normalizeBaseUrl(baseUrl) + "/api/v1" + path; + } + @Override public void close() { try { 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 ba774745d056..a5056b045f95 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 @@ -51,8 +51,20 @@ public boolean validate(DnsServer server) { } @Override - public boolean createZone(DnsServer server, DnsZone zone) { - return false; + public boolean provisionZone(DnsServer server, DnsZone zone) throws Exception { + if (StringUtils.isBlank(zone.getName())) { + throw new IllegalArgumentException("Zone name cannot be empty"); + } + + if (StringUtils.isBlank(server.getUrl())) { + throw new IllegalArgumentException("PowerDNS API URL cannot be empty"); + } + + if (StringUtils.isBlank(server.getApiKey())) { + throw new IllegalArgumentException("PowerDNS API key cannot be empty"); + } + client.createZone(server.getUrl(), server.getApiKey(), zone.getName(), null); + return true; } @Override 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 4d5c0e5d1cae..7192261087d9 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -39,7 +39,9 @@ import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.dns.dao.DnsServerDao; +import org.apache.cloudstack.dns.dao.DnsZoneDao; import org.apache.cloudstack.dns.vo.DnsServerVO; +import org.apache.cloudstack.dns.vo.DnsZoneVO; import org.springframework.stereotype.Component; import com.cloud.exception.InvalidParameterValueException; @@ -59,6 +61,8 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa AccountManager accountMgr; @Inject DnsServerDao dnsServerDao; + @Inject + DnsZoneDao dnsZoneDao; private DnsProvider getProvider(DnsProviderType type) { if (type == null) { @@ -272,18 +276,67 @@ public List listProviderNames() { } @Override - public DnsZone allocDnsZone(CreateDnsZoneCmd cmd) { - return null; + public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); + DnsServerVO server = dnsServerDao.findById(cmd.getDnsServerId()); + if (server == null) { + throw new InvalidParameterValueException("DNS Server not found"); + } + boolean isOwner = (server.getAccountId() == caller.getId()); + if (!server.isPublic() && !isOwner) { + throw new PermissionDeniedException("You do not have permission to use this DNS Server."); + } + DnsZone.ZoneType type = DnsZone.ZoneType.Public; + if (cmd.getType() != null) { + try { + type = DnsZone.ZoneType.valueOf(cmd.getType()); + } catch (IllegalArgumentException e) { + throw new InvalidParameterValueException("Invalid Zone Type"); + } + } + DnsZoneVO existing = dnsZoneDao.findByNameServerAndType(cmd.getName(), server.getId(), type); + if (existing != null) { + throw new InvalidParameterValueException("Zone already exists on this server."); + } + DnsZoneVO dnsZoneVO = new DnsZoneVO(cmd.getName(), type, server.getId(), caller.getId(), caller.getDomainId(), cmd.getDescription()); + return dnsZoneDao.persist(dnsZoneVO); } @Override public DnsZone provisionDnsZone(long zoneId) { - return null; + DnsZoneVO dnsZone = dnsZoneDao.findById(zoneId); + if (dnsZone == null) { + throw new CloudRuntimeException("DNS Zone not found during provisioning"); + } + DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId()); + + try { + DnsProvider provider = getProvider(server.getProviderType()); + logger.debug("Provision DNS zone: {} on DNS server: {}", dnsZone.getName(), server.getName()); + boolean success = provider.provisionZone(server, dnsZone); + if (success) { + dnsZone.setState(DnsZone.State.Active); + dnsZoneDao.update(dnsZone.getId(), dnsZone); + return dnsZone; + } else { + logger.error("DNS provider failed to provision zone: {}", dnsZone.getName()); + throw new CloudRuntimeException("DNS provider failed to provision zone"); + } + } catch (Exception ex) { + logger.error("Failed to provision zone: {} on server: {}", dnsZone.getName(), server.getName(), ex); + dnsZoneDao.remove(zoneId); + throw new CloudRuntimeException("Failed to provision zone: " + dnsZone.getName()); + } } @Override public DnsZoneResponse createDnsZoneResponse(DnsZone zone) { - return null; + DnsZoneResponse res = new DnsZoneResponse(); + res.setName(zone.getName()); + res.setDnsServerId(zone.getDnsServerId()); + res.setType(zone.getType()); + res.setState(zone.getState()); + return res; } @Override diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java index 30d341021439..d618597757de 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java @@ -19,10 +19,13 @@ import java.util.List; +import org.apache.cloudstack.dns.DnsZone; import org.apache.cloudstack.dns.vo.DnsZoneVO; import com.cloud.utils.db.GenericDao; public interface DnsZoneDao extends GenericDao { List listByServerId(long serverId); + List listByAccount(long accountId); + DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java index 1a573e4132ae..cb693338ef53 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java @@ -19,6 +19,8 @@ import java.util.List; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.DnsZone; import org.apache.cloudstack.dns.vo.DnsZoneVO; import org.springframework.stereotype.Component; @@ -30,12 +32,24 @@ public class DnsZoneDaoImpl extends GenericDaoBase implements DnsZoneDao { static final String DNS_SERVER_ID = "dnsServerId"; SearchBuilder ServerSearch; + SearchBuilder AccountSearch; + SearchBuilder NameServerTypeSearch; public DnsZoneDaoImpl() { super(); ServerSearch = createSearchBuilder(); ServerSearch.and(DNS_SERVER_ID, ServerSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); ServerSearch.done(); + + AccountSearch = createSearchBuilder(); + AccountSearch.and(ApiConstants.ACCOUNT_ID, AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountSearch.done(); + + NameServerTypeSearch = createSearchBuilder(); + NameServerTypeSearch.and(ApiConstants.NAME, NameServerTypeSearch.entity().getName(), SearchCriteria.Op.EQ); + NameServerTypeSearch.and(DNS_SERVER_ID, NameServerTypeSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); + NameServerTypeSearch.and(ApiConstants.TYPE, NameServerTypeSearch.entity().getType(), SearchCriteria.Op.EQ); + NameServerTypeSearch.done(); } @Override @@ -44,4 +58,20 @@ public List listByServerId(long serverId) { sc.setParameters(DNS_SERVER_ID, serverId); return listBy(sc); } + + @Override + public List listByAccount(long accountId) { + SearchCriteria sc = AccountSearch.create(); + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + return listBy(sc); + } + + @Override + public DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type) { + SearchCriteria sc = NameServerTypeSearch.create(); + sc.setParameters(ApiConstants.NAME, name); + sc.setParameters(DNS_SERVER_ID, dnsServerId); + sc.setParameters(ApiConstants.TYPE, type); + return findOneBy(sc); + } } diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java index 4495081cd209..0d47c1abad0c 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java @@ -53,6 +53,15 @@ public class DnsZoneVO implements DnsZone { @Column(name = "dns_server_id") private long dnsServerId; + @Column(name = "account_id") + private long accountId; + + @Column(name = "domain_id") + private long domainId; + + @Column(name = "description") + private String description; + @Column(name = "external_reference") private String externalReference; @@ -64,9 +73,6 @@ public class DnsZoneVO implements DnsZone { @Enumerated(EnumType.STRING) private State state; - @Column(name = "account_id") - private long accountId; - @Column(name = GenericDao.CREATED_COLUMN) @Temporal(value = TemporalType.TIMESTAMP) private Date created = null; @@ -78,14 +84,22 @@ public class DnsZoneVO implements DnsZone { public DnsZoneVO() { this.uuid = UUID.randomUUID().toString(); this.created = new Date(); + this.state = State.Inactive; } - public DnsZoneVO(String name, long dnsServerId, long accountId) { + public DnsZoneVO(String name, ZoneType type, long dnsServerId, long accountId, long domainId, String description) { this(); this.name = name; + this.type = (type != null) ? type : ZoneType.Public; this.dnsServerId = dnsServerId; this.accountId = accountId; - this.type = ZoneType.Public; + this.domainId = domainId; + this.description = description; + } + + @Override + public Class getEntityType() { + return DnsZone.class; } @Override @@ -108,11 +122,21 @@ public ZoneType getType() { return type; } + @Override + public String getDescription() { + return description; + } + @Override public List getAssociatedNetworks() { return List.of(); } + @Override + public State getState() { + return state; + } + @Override public String getUuid() { return uuid; @@ -122,4 +146,11 @@ public String getUuid() { public long getId() { return id; } + + @Override + public long getDomainId() { + return domainId; + } + + public void setState(State state) { this.state = state; } } From f29b8be24d790745e3c10caf02eb94433a74d662 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Sun, 15 Feb 2026 15:17:54 +0530 Subject: [PATCH 06/23] following dns zone apis are integrated: 1. creatednszone 2. listdnszone 3. updatednszone 4. deletednszone --- .../command/user/dns/DeleteDnsZoneCmd.java | 9 +- .../api/command/user/dns/ListDnsZonesCmd.java | 14 ++- .../command/user/dns/UpdateDnsZoneCmd.java | 78 ++++++++++++++++ .../api/response/DnsZoneResponse.java | 12 +++ .../apache/cloudstack/dns/DnsProvider.java | 6 +- .../cloudstack/dns/DnsProviderManager.java | 17 ++-- .../META-INF/db/schema-42210to42300.sql | 1 - .../dns/powerdns/PowerDnsClient.java | 88 +++++++++++++------ .../dns/powerdns/PowerDnsProvider.java | 49 ++++++----- .../dns/DnsProviderManagerImpl.java | 85 ++++++++++++++---- .../apache/cloudstack/dns/dao/DnsZoneDao.java | 3 + .../cloudstack/dns/dao/DnsZoneDaoImpl.java | 28 ++++++ .../apache/cloudstack/dns/vo/DnsZoneVO.java | 4 + 13 files changed, 302 insertions(+), 92 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java index b15357620c0f..6b17f5249f47 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java @@ -23,8 +23,7 @@ public class DeleteDnsZoneCmd extends BaseAsyncCmd { //////////////// API Parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, - required = true, description = "the ID of the DNS zone") + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, description = "The ID of the DNS zone") private Long id; ///////////////////////////////////////////////////// @@ -42,9 +41,7 @@ public Long getId() { @Override public void execute() { try { - // The Manager handles both DB removal and Plugin execution - boolean result = dnsProviderManager.deleteDnsZone(this); - + boolean result = dnsProviderManager.deleteDnsZone(getId()); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); setResponseObject(response); @@ -74,7 +71,7 @@ public long getEntityOwnerId() { @Override public String getEventType() { - return EventTypes.EVENT_DNS_ZONE_DELETE; // Ensure this constant is added to EventTypes + return EventTypes.EVENT_DNS_ZONE_DELETE; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java index 0d7750c339c6..edc65eef9262 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java @@ -7,9 +7,8 @@ import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; -import org.apache.cloudstack.api.response.NetworkResponse; -@APICommand(name = "listDnsZones", description = "Lists DNS Zones.", +@APICommand(name = "listDnsZones", description = "Lists DNS zones.", responseObject = DnsZoneResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class ListDnsZonesCmd extends BaseListAccountResourcesCmd { @@ -20,20 +19,19 @@ public class ListDnsZonesCmd extends BaseListAccountResourcesCmd { ///////////////////////////////////////////////////// /// @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, - description = "list DNS zone by ID") + description = "List DNS zone by ID") private Long id; @Parameter(name = "dnsserverid", type = CommandType.UUID, entityType = DnsServerResponse.class, - description = "list DNS zones belonging to a specific DNS Server") + description = "List DNS zones belonging to a specific DNS server") private Long dnsServerId; - @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, - description = "list DNS zones associated with a specific Network") - private Long networkId; + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "List by zone name") + private String name; public Long getId() { return id; } public Long getDnsServerId() { return dnsServerId; } - public Long getNetworkId() { return networkId; } + public String getName() { return name; } @Override public void execute() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java new file mode 100644 index 000000000000..d679db706461 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java @@ -0,0 +1,78 @@ +package org.apache.cloudstack.api.command.user.dns; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderManager; +import org.apache.cloudstack.dns.DnsZone; + +@APICommand(name = "updateDnsZone", description = "Updates a DNS Zone's metadata", + responseObject = DnsZoneResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class UpdateDnsZoneCmd extends BaseCmd { + + private static final String COMMAND_RESPONSE_NAME = "updatednszoneresponse"; + + @Inject + DnsProviderManager dnsProviderManager; + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "The ID of the DNS zone") + private Long id; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "Display text for the zone") + private String description; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getDescription() { + return description; + } + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + try { + DnsZone result = dnsProviderManager.updateDnsZone(this); + if (result != null) { + DnsZoneResponse response = dnsProviderManager.createDnsZoneResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update DNS Zone on external provider"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update DNS Zone: " + e.getMessage()); + } + } + + @Override + public String getCommandName() { + return COMMAND_RESPONSE_NAME; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java index f8ef89824c4d..a2b19d32fa19 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java @@ -59,6 +59,10 @@ public class DnsZoneResponse extends BaseResponse { @Param(description = "The state of the zone (Active/Inactive)") private DnsZone.State state; + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Description for the DNS zone") + private String description; + public DnsZoneResponse() { super(); setObjectName("dnszone"); @@ -91,4 +95,12 @@ public void setType(DnsZone.ZoneType type) { public void setState(DnsZone.State state) { this.state = state; } + + public void setId(String id) { + this.id = id; + } + + public void setDescription(String description) { + this.description = description; + } } 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 68ebd2a682d9..74c74ad8a5fe 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -25,11 +25,11 @@ public interface DnsProvider extends Adapter { DnsProviderType getProviderType(); // Validates connectivity to the server - boolean validate(DnsServer server) throws Exception; + void validate(DnsServer server) throws Exception; // Zone Operations - boolean provisionZone(DnsServer server, DnsZone zone) throws Exception; - boolean deleteZone(DnsServer server, DnsZone zone); + void provisionZone(DnsServer server, DnsZone zone); + void deleteZone(DnsServer server, DnsZone zone) ; DnsRecord createRecord(DnsServer server, DnsZone zone, DnsRecord record); boolean updateRecord(DnsServer server, DnsZone zone, DnsRecord record); 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 0421e8fffb6e..fc4bb004df67 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -24,11 +24,11 @@ import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; import org.apache.cloudstack.api.command.user.dns.DeleteDnsRecordCmd; import org.apache.cloudstack.api.command.user.dns.DeleteDnsServerCmd; -import org.apache.cloudstack.api.command.user.dns.DeleteDnsZoneCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.UpdateDnsZoneCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; @@ -47,8 +47,14 @@ public interface DnsProviderManager extends Manager, PluggableService { DnsServer getDnsServer(Long id); - DnsZone createDnsZone(CreateDnsZoneCmd cmd); - boolean deleteDnsZone(DeleteDnsZoneCmd cmd); + // Allocates the DB row (State: Inactive) + DnsZone allocateDnsZone(CreateDnsZoneCmd cmd); + // Calls the Plugin (State: Inactive -> Active) + DnsZone provisionDnsZone(long zoneId); + + DnsZone getDnsZone(Long id); + DnsZone updateDnsZone(UpdateDnsZoneCmd cmd); + boolean deleteDnsZone(Long id); ListResponse listDnsZones(ListDnsZonesCmd cmd); DnsZone getDnsZone(long id); @@ -60,11 +66,6 @@ public interface DnsProviderManager extends Manager, PluggableService { List listProviderNames(); - // Allocates the DB row (State: Inactive) - DnsZone allocateDnsZone(CreateDnsZoneCmd cmd); - - // Calls the Plugin (State: Inactive -> Active) - DnsZone provisionDnsZone(long zoneId); // Helper to create the response object DnsZoneResponse createDnsZoneResponse(DnsZone zone); 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 7eb199c02055..e0731301bbce 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 @@ -92,7 +92,6 @@ CREATE TABLE `cloud`.`dns_zone` ( `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', PRIMARY KEY (`id`), CONSTRAINT `uc_dns_zone__uuid` UNIQUE (`uuid`), - CONSTRAINT `uc_dns_zone__name_server_type` UNIQUE (`name`, `dns_server_id`, `type`), KEY `i_dns_zone__dns_server` (`dns_server_id`), KEY `i_dns_zone__account_id` (`account_id`), CONSTRAINT `fk_dns_zone__dns_server_id` FOREIGN KEY (`dns_server_id`) REFERENCES `dns_server` (`id`) ON DELETE CASCADE, 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 23b23e7e7e23..244cef704b47 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 @@ -18,11 +18,14 @@ package org.apache.cloudstack.dns.powerdns; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; import org.apache.http.HttpStatus; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; @@ -45,16 +48,15 @@ public class PowerDnsClient implements AutoCloseable { private final CloseableHttpClient httpClient; public void validate(String baseUrl, String apiKey) { - String checkUrl = buildApiUrl(baseUrl, "/api/v1/servers"); + String checkUrl = buildApiUrl(baseUrl, "/servers"); HttpGet request = new HttpGet(checkUrl); request.addHeader("X-API-Key", apiKey); + request.addHeader("Content-Type", "application/json"); request.addHeader("Accept", "application/json"); try (CloseableHttpResponse response = httpClient.execute(request)) { int statusCode = response.getStatusLine().getStatusCode(); - String body = response.getEntity() != null - ? EntityUtils.toString(response.getEntity()) - : null; + String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; if (statusCode == HttpStatus.SC_OK) { JsonNode root = MAPPER.readTree(body); @@ -88,27 +90,25 @@ public void validate(String baseUrl, String apiKey) { } public void createZone(String baseUrl, String apiKey, String zoneName, List nameservers) { - String url = buildApiUrl(baseUrl, "/servers/localhost/zones"); - ObjectNode json = MAPPER.createObjectNode(); - json.put("name", zoneName.endsWith(".") ? zoneName : zoneName + "."); - json.put("kind", "Native"); - json.put("dnssec", false); - - if (nameservers != null && !nameservers.isEmpty()) { - ArrayNode nsArray = json.putArray("nameservers"); - for (String ns : nameservers) { - nsArray.add(ns.endsWith(".") ? ns : ns + "."); + String normalizedZone = zoneName.endsWith(".") ? zoneName : zoneName + "."; + try { + String url = buildApiUrl(baseUrl, "/servers/localhost/zones"); + ObjectNode json = MAPPER.createObjectNode(); + json.put("name", normalizedZone); + json.put("kind", "Native"); + json.put("dnssec", false); + + if (nameservers != null && !nameservers.isEmpty()) { + ArrayNode nsArray = json.putArray("nameservers"); + for (String ns : nameservers) { + nsArray.add(ns.endsWith(".") ? ns : ns + "."); + } } - } - - logger.debug("Creating PowerDNS zone: {} using URL: {}", zoneName, url); - - HttpPost request = new HttpPost(url); - request.addHeader("X-API-Key", apiKey); - request.addHeader("Content-Type", "application/json"); - request.addHeader("Accept", "application/json"); - try { + HttpPost request = new HttpPost(url); + request.addHeader("X-API-Key", apiKey); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Accept", "application/json"); request.setEntity(new StringEntity(json.toString())); try (CloseableHttpResponse response = httpClient.execute(request)) { @@ -127,21 +127,55 @@ public void createZone(String baseUrl, String apiKey, String zoneName, List listRecords(DnsServer server, DnsZone zone) { return List.of(); } + void validateServerZoneParams(DnsServer server, DnsZone zone) { + validateServerParams(server); + if (StringUtils.isBlank(zone.getName())) { + throw new IllegalArgumentException("Zone name cannot be empty"); + } + } + + void validateServerParams(DnsServer server) { + if (StringUtils.isBlank(server.getUrl())) { + throw new IllegalArgumentException("PowerDNS API URL cannot be empty"); + } + if (StringUtils.isBlank(server.getApiKey())) { + throw new IllegalArgumentException("PowerDNS API key cannot be empty"); + } + } + + + @Override public boolean configure(String name, Map params) { if (client == 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 7192261087d9..6cc1b62e13d0 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -22,6 +22,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; import org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd; import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; @@ -33,6 +34,7 @@ import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.UpdateDnsZoneCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; @@ -84,7 +86,6 @@ public DnsServer addDnsServer(AddDnsServerCmd cmd) { throw new InvalidParameterValueException( "This Account already has a DNS Server integration for URL: " + cmd.getUrl()); } - DnsProviderType type = DnsProviderType.fromString(cmd.getProvider()); DnsProvider provider = getProvider(type); DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), type, cmd.getCredentials(), cmd.getPort(), @@ -230,18 +231,74 @@ public DnsServer getDnsServer(Long id) { } @Override - public DnsZone createDnsZone(CreateDnsZoneCmd cmd) { - return null; + public boolean deleteDnsZone(Long zoneId) { + DnsZoneVO zone = dnsZoneDao.findById(zoneId); + if (zone == null) { + throw new InvalidParameterValueException("DNS Zone with ID " + zoneId + " not found."); + } + + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, true, zone); + DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); + if (server != null && zone.getState() == DnsZone.State.Active) { + try { + DnsProvider provider = getProvider(server.getProviderType()); + logger.debug("Deleting DNS zone {} from provider.", zone.getName()); + provider.deleteZone(server, zone); + } catch (Exception ex) { + logger.error("Failed to delete zone from provider", ex); + throw new CloudRuntimeException("Failed to delete DNS zone."); + } + } + return dnsZoneDao.remove(zoneId); } @Override - public boolean deleteDnsZone(DeleteDnsZoneCmd cmd) { - return false; + public DnsZone getDnsZone(Long id) { + return dnsZoneDao.findById(id); + } + + @Override + public DnsZone updateDnsZone(UpdateDnsZoneCmd cmd) { + DnsZoneVO zone = dnsZoneDao.findById(cmd.getId()); + if (zone == null) { + throw new InvalidParameterValueException("DNS zone not found."); + } + + // ACL Check + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, true, zone); + + // Update fields + boolean updated = false; + if (cmd.getDescription() != null) { + zone.setDescription(cmd.getDescription()); + updated = true; + } + + if (updated) { + dnsZoneDao.update(zone.getId(), zone); + } + return zone; } @Override public ListResponse listDnsZones(ListDnsZonesCmd cmd) { - return null; + Account caller = CallContext.current().getCallingAccount(); + Filter searchFilter = new Filter(DnsZoneVO.class, ApiConstants.ID, true, cmd.getStartIndex(), cmd.getPageSizeVal()); + Long accountIdFilter = caller.getAccountId(); + String keyword = cmd.getKeyword(); + if (cmd.getName() != null) { + keyword = cmd.getName(); + } + Pair, Integer> result = dnsZoneDao.searchZones(cmd.getId(), cmd.getDnsServerId(), keyword, accountIdFilter, searchFilter); + List zoneResponses = new ArrayList<>(); + for (DnsZoneVO zone : result.first()) { + zoneResponses.add(createDnsZoneResponse(zone)); + } + ListResponse response = new ListResponse<>(); + response.setResponses(zoneResponses, result.second()); + return response; } @Override @@ -313,20 +370,15 @@ public DnsZone provisionDnsZone(long zoneId) { try { DnsProvider provider = getProvider(server.getProviderType()); logger.debug("Provision DNS zone: {} on DNS server: {}", dnsZone.getName(), server.getName()); - boolean success = provider.provisionZone(server, dnsZone); - if (success) { - dnsZone.setState(DnsZone.State.Active); - dnsZoneDao.update(dnsZone.getId(), dnsZone); - return dnsZone; - } else { - logger.error("DNS provider failed to provision zone: {}", dnsZone.getName()); - throw new CloudRuntimeException("DNS provider failed to provision zone"); - } + provider.provisionZone(server, dnsZone); + dnsZone.setState(DnsZone.State.Active); + dnsZoneDao.update(dnsZone.getId(), dnsZone); } catch (Exception ex) { logger.error("Failed to provision zone: {} on server: {}", dnsZone.getName(), server.getName(), ex); dnsZoneDao.remove(zoneId); throw new CloudRuntimeException("Failed to provision zone: " + dnsZone.getName()); } + return dnsZone; } @Override @@ -336,6 +388,8 @@ public DnsZoneResponse createDnsZoneResponse(DnsZone zone) { res.setDnsServerId(zone.getDnsServerId()); res.setType(zone.getType()); res.setState(zone.getState()); + res.setId(zone.getUuid()); + res.setDescription(zone.getDescription()); return res; } @@ -364,6 +418,7 @@ public List> getCommands() { cmdList.add(CreateDnsZoneCmd.class); cmdList.add(ListDnsZonesCmd.class); cmdList.add(DeleteDnsZoneCmd.class); + cmdList.add(UpdateDnsZoneCmd.class); // DNS Record Commands cmdList.add(CreateDnsRecordCmd.class); diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java index d618597757de..a2489323e8e3 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java @@ -22,10 +22,13 @@ import org.apache.cloudstack.dns.DnsZone; import org.apache.cloudstack.dns.vo.DnsZoneVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; public interface DnsZoneDao extends GenericDao { List listByServerId(long serverId); List listByAccount(long accountId); DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type); + Pair, Integer> searchZones(Long id, Long dnsServerId, String keyword, Long accountId, Filter filter); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java index cb693338ef53..b45d10723a19 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java @@ -24,6 +24,8 @@ import org.apache.cloudstack.dns.vo.DnsZoneVO; import org.springframework.stereotype.Component; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @@ -34,6 +36,7 @@ public class DnsZoneDaoImpl extends GenericDaoBase implements D SearchBuilder ServerSearch; SearchBuilder AccountSearch; SearchBuilder NameServerTypeSearch; + SearchBuilder AllFieldsSearch; public DnsZoneDaoImpl() { super(); @@ -50,6 +53,13 @@ public DnsZoneDaoImpl() { NameServerTypeSearch.and(DNS_SERVER_ID, NameServerTypeSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); NameServerTypeSearch.and(ApiConstants.TYPE, NameServerTypeSearch.entity().getType(), SearchCriteria.Op.EQ); NameServerTypeSearch.done(); + + AllFieldsSearch = createSearchBuilder(); + AllFieldsSearch.and(ApiConstants.ID, AllFieldsSearch.entity().getId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and(DNS_SERVER_ID, AllFieldsSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and(ApiConstants.NAME, AllFieldsSearch.entity().getName(), SearchCriteria.Op.LIKE); + AllFieldsSearch.and(ApiConstants.ACCOUNT_ID, AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AllFieldsSearch.done(); } @Override @@ -74,4 +84,22 @@ public DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone. sc.setParameters(ApiConstants.TYPE, type); return findOneBy(sc); } + + @Override + public Pair, Integer> searchZones(Long id, Long dnsServerId, String keyword, Long accountId, Filter filter) { + SearchCriteria sc = AllFieldsSearch.create(); + if (id != null) { + sc.setParameters(ApiConstants.ID, id); + } + if (dnsServerId != null) { + sc.setParameters(DNS_SERVER_ID, dnsServerId); + } + if (keyword != null) { + sc.setParameters(ApiConstants.NAME, "%" + keyword + "%"); + } + if (accountId != null) { + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + } + return searchAndCount(sc, filter); + } } diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java index 0d47c1abad0c..2988ee49acd4 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java @@ -153,4 +153,8 @@ public long getDomainId() { } public void setState(State state) { this.state = state; } + + public void setDescription(String description) { + this.description = description; + } } From 9b77c3708e8b31af826067f4f8d7d6f109627cb3 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Mon, 16 Feb 2026 16:40:19 +0530 Subject: [PATCH 07/23] Tested following flow: 1. Add dns server 2. create zone 3. add records 4. verify in powerdns 5. verify using dig --- .../apache/cloudstack/api/ApiConstants.java | 2 + .../api/command/user/dns/AddDnsServerCmd.java | 11 +- .../command/user/dns/CreateDnsRecordCmd.java | 29 ++- .../command/user/dns/DeleteDnsRecordCmd.java | 17 +- .../command/user/dns/ListDnsRecordsCmd.java | 11 +- .../command/user/dns/UpdateDnsServerCmd.java | 17 ++ .../api/response/DnsRecordResponse.java | 36 +-- .../apache/cloudstack/dns/DnsProvider.java | 7 +- .../cloudstack/dns/DnsProviderManager.java | 3 +- .../org/apache/cloudstack/dns/DnsRecord.java | 14 +- .../org/apache/cloudstack/dns/DnsServer.java | 4 +- .../dns/powerdns/PowerDnsClient.java | 213 +++++++++++++++++- .../dns/powerdns/PowerDnsProvider.java | 55 ++++- .../dns/DnsProviderManagerImpl.java | 82 ++++++- .../apache/cloudstack/dns/vo/DnsServerVO.java | 9 +- 15 files changed, 428 insertions(+), 82 deletions(-) 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 a64e40fa7a2a..5dbb200d32ad 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1336,6 +1336,8 @@ public class ApiConstants { // DNS provider related public static final String NAME_SERVERS = "nameservers"; public static final String CREDENTIALS = "credentials"; + public static final String DNS_ZONE_ID = "dnszoneid"; + public static final String CONTENTS = "contents"; public static final String PUBLIC_DOMAIN_SUFFIX = "publicdomainsuffix"; 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/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java index 764992d6e811..e5e38b7cbda5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -17,6 +17,8 @@ package org.apache.cloudstack.api.command.user.dns; +import java.util.List; + import javax.inject.Inject; import org.apache.cloudstack.api.APICommand; @@ -51,7 +53,7 @@ public class AddDnsServerCmd extends BaseCmd { @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") private String provider; - @Parameter(name = ApiConstants.CREDENTIALS, type = CommandType.STRING, required = false, description = "API Key or Credentials for the external provider") + @Parameter(name = ApiConstants.CREDENTIALS, type = CommandType.STRING, description = "API Key or Credentials for the external provider") private String credentials; @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") @@ -63,8 +65,9 @@ public class AddDnsServerCmd extends BaseCmd { @Parameter(name = ApiConstants.PUBLIC_DOMAIN_SUFFIX, type = CommandType.STRING, description = "The domain suffix used for public access (e.g. public.example.com)") private String publicDomainSuffix; - @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.STRING, description = "Comma separated list of name servers") - private String nameServers; + @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.LIST, collectionType = CommandType.STRING, + required = true, description = "Comma separated list of name servers") + private List nameServers; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -89,7 +92,7 @@ public String getPublicDomainSuffix() { return publicDomainSuffix; } - public String getNameServers() { + public List getNameServers() { return nameServers; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java index b041c46b72eb..2967fabb9f0c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java @@ -1,5 +1,7 @@ package org.apache.cloudstack.api.command.user.dns; +import java.util.List; + import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -9,15 +11,19 @@ import org.apache.cloudstack.api.response.DnsRecordResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsRecord; import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; @APICommand(name = "createDnsRecord", description = "Creates a DNS record directly on the provider", responseObject = DnsRecordResponse.class) public class CreateDnsRecordCmd extends BaseAsyncCmd { - @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true) - private Long zoneId; + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, + description = "ID of the DNS zone") + private Long dnsZoneId; @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "Record name") private String name; @@ -25,19 +31,28 @@ public class CreateDnsRecordCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "Record type (A, CNAME)") private String type; - @Parameter(name = "content", type = CommandType.STRING, required = true, description = "IP or target") - private String content; + @Parameter(name = ApiConstants.CONTENTS, type = CommandType.LIST, collectionType = CommandType.STRING, required = true, + description = "The content of the record (IP address for A/AAAA, FQDN for CNAME/NS, quoted string for TXT, etc.)") + private List contents; @Parameter(name = "ttl", type = CommandType.INTEGER, description = "Time to live") private Integer ttl; // Getters - public Long getZoneId() { return zoneId; } + public Long getDnsZoneId() { return dnsZoneId; } public String getName() { return name; } - public String getType() { return type; } - public String getContent() { return content; } + + public List getContents() { return contents; } public Integer getTtl() { return (ttl == null) ? 3600 : ttl; } + public DnsRecord.RecordType getType() { + DnsRecord.RecordType dnsRecordType = EnumUtils.getEnumIgnoreCase(DnsRecord.RecordType.class, type); + if (dnsRecordType == null) { + throw new InvalidParameterValueException("Invalid value passed for record type, valid values are: " + EnumUtils.listValues(DnsRecord.RecordType.values())); + } + return dnsRecordType; + } + @Override public void execute() { try { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java index 3b0ea4a73eec..79e688db1ca1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java @@ -9,16 +9,19 @@ import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsRecord; import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; @APICommand(name = "deleteDnsRecord", description = "Deletes a DNS record from the external provider", responseObject = SuccessResponse.class) public class DeleteDnsRecordCmd extends BaseAsyncCmd { - @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, description = "The ID of the DNS zone") - private Long zoneId; + private Long dnsZoneId; @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true) private String name; @@ -27,9 +30,15 @@ public class DeleteDnsRecordCmd extends BaseAsyncCmd { private String type; // Getters - public Long getZoneId() { return zoneId; } + public DnsRecord.RecordType getType() { + DnsRecord.RecordType dnsRecordType = EnumUtils.getEnumIgnoreCase(DnsRecord.RecordType.class, type); + if (dnsRecordType == null) { + throw new InvalidParameterValueException("Invalid value passed for record type, valid values are: " + EnumUtils.listValues(DnsRecord.RecordType.values())); + } + return dnsRecordType; + } + public Long getDnsZoneId() { return dnsZoneId; } public String getName() { return name; } - public String getType() { return type; } @Override public void execute() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java index a0c401250eda..c419ef98cc1e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java @@ -12,12 +12,13 @@ responseObject = DnsRecordResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class ListDnsRecordsCmd extends BaseListCmd { - @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, - required = true, description = "the ID of the DNS zone to list records from") - private Long zoneId; - public Long getZoneId() { - return zoneId; + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, + description = "ID of the DNS zone to list records from") + private Long dnsZoneId; + + public Long getDnsZoneId() { + return dnsZoneId; } @Override diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java index c9f63b7f06a1..051acc286007 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java @@ -30,6 +30,9 @@ import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.dns.DnsServer; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.utils.EnumUtils; @APICommand(name = "updateDnsServer", description = "Update DNS server", responseObject = DnsServerResponse.class, requestHasSensitiveInfo = true) @@ -67,6 +70,9 @@ public class UpdateDnsServerCmd extends BaseCmd { @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.STRING, description = "Comma separated list of name servers") private String nameServers; + @Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "Update state for the DNS server (Enabled, Disabled)") + private String state; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -109,4 +115,15 @@ public void execute() { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); } } + + public DnsServer.State getState() { + if (StringUtils.isBlank(state)) { + return null; + } + DnsServer.State dnsState = EnumUtils.getEnumIgnoreCase(DnsServer.State.class, state); + if (dnsState == null) { + throw new IllegalArgumentException("Invalid state value: " + state); + } + return dnsState; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java index 578a0f940723..6e2082485512 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java @@ -17,6 +17,8 @@ package org.apache.cloudstack.api.response; +import java.util.List; + import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.dns.DnsRecord; @@ -27,29 +29,21 @@ public class DnsRecordResponse extends BaseResponse { @SerializedName(ApiConstants.NAME) - @Param(description = "the name of the DNS record") + @Param(description = "The record name (e.g., www.example.com.)") private String name; @SerializedName(ApiConstants.TYPE) - @Param(description = "the type of the DNS record (A, CNAME, etc)") - private String type; + @Param(description = "The record type (e.g., A, CNAME, TXT)") + private DnsRecord.RecordType type; - @SerializedName("content") - @Param(description = "the content of the record (IP address or target)") - private String content; + @SerializedName("contents") + @Param(description = "The contents of the record (IP address or target)") + private List contents; @SerializedName("ttl") - @Param(description = "the time to live (TTL) in seconds") + @Param(description = "Time to live (TTL) in seconds") private Integer ttl; - @SerializedName(ApiConstants.ZONE_ID) - @Param(description = "the ID of the zone this record belongs to") - private String zoneId; - - @SerializedName("sourceid") - @Param(description = "the external ID of the record on the provider") - private String sourceId; - public DnsRecordResponse() { super(); setObjectName("dnsrecord"); @@ -57,15 +51,7 @@ public DnsRecordResponse() { // Setters public void setName(String name) { this.name = name; } - - // Accepts String or Enum.toString() - public void setType(String type) { this.type = type; } - public void setType(DnsRecord.RecordType type) { - this.type = (type != null) ? type.name() : null; - } - - public void setContent(String content) { this.content = content; } + public void setType(DnsRecord.RecordType type) { this.type = type; } + public void setContent(List contents) { this.contents = contents; } public void setTtl(Integer ttl) { this.ttl = ttl; } - public void setZoneId(String zoneId) { this.zoneId = zoneId; } - public void setSourceId(String sourceId) { this.sourceId = sourceId; } } 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 74c74ad8a5fe..f03f391f46c2 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -31,9 +31,8 @@ public interface DnsProvider extends Adapter { void provisionZone(DnsServer server, DnsZone zone); void deleteZone(DnsServer server, DnsZone zone) ; - DnsRecord createRecord(DnsServer server, DnsZone zone, DnsRecord record); - boolean updateRecord(DnsServer server, DnsZone zone, DnsRecord record); - boolean deleteRecord(DnsServer server, DnsZone zone, DnsRecord record); - + void addRecord(DnsServer server, DnsZone zone, DnsRecord record); List listRecords(DnsServer server, DnsZone zone); + void updateRecord(DnsServer server, DnsZone zone, DnsRecord record); + void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record); } 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 fc4bb004df67..b918b49fb39e 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -56,12 +56,10 @@ public interface DnsProviderManager extends Manager, PluggableService { DnsZone updateDnsZone(UpdateDnsZoneCmd cmd); boolean deleteDnsZone(Long id); ListResponse listDnsZones(ListDnsZonesCmd cmd); - DnsZone getDnsZone(long id); DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd); boolean deleteDnsRecord(DeleteDnsRecordCmd cmd); - ListResponse listDnsRecords(ListDnsRecordsCmd cmd); List listProviderNames(); @@ -69,4 +67,5 @@ public interface DnsProviderManager extends Manager, PluggableService { // Helper to create the response object DnsZoneResponse createDnsZoneResponse(DnsZone zone); + DnsRecordResponse createDnsRecordResponse(DnsRecord record); } diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java b/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java index 7ee6b51c3f2d..ae62e4729cca 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java @@ -17,6 +17,8 @@ package org.apache.cloudstack.dns; +import java.util.List; + import com.cloud.utils.exception.CloudRuntimeException; public class DnsRecord { @@ -36,16 +38,16 @@ public static RecordType fromString(String type) { } private String name; - private RecordType type; // Enforced Enum here - private String content; + private RecordType type; + private List contents; private int ttl; public DnsRecord() {} - public DnsRecord(String name, RecordType type, String content, int ttl) { + public DnsRecord(String name, RecordType type, List contents, int ttl) { this.name = name; this.type = type; - this.content = content; + this.contents = contents; this.ttl = ttl; } @@ -56,8 +58,8 @@ public DnsRecord(String name, RecordType type, String content, int ttl) { public RecordType getType() { return type; } public void setType(RecordType type) { this.type = type; } - public String getContent() { return content; } - public void setContent(String content) { this.content = content; } + public List getContents() { return contents; } + public void setContents(List contents) { this.contents = contents; } public int getTtl() { return ttl; } public void setTtl(int ttl) { this.ttl = ttl; } diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java index dafd40017e57..617352cbfe38 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java @@ -24,7 +24,7 @@ public interface DnsServer extends InternalIdentity, Identity { enum State { - Enabled, Disabled; + Enabled, Disabled }; String getName(); @@ -33,6 +33,8 @@ enum State { DnsProviderType getProviderType(); + String getNameServers(); + String getApiKey(); long getAccountId(); 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 244cef704b47..a69e2e7f165e 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 @@ -20,13 +20,18 @@ import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import org.apache.commons.collections.CollectionUtils; import org.apache.http.HttpStatus; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; @@ -35,6 +40,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.cloud.utils.StringUtils; import com.cloud.utils.exception.CloudRuntimeException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -89,8 +95,8 @@ public void validate(String baseUrl, String apiKey) { } } - public void createZone(String baseUrl, String apiKey, String zoneName, List nameservers) { - String normalizedZone = zoneName.endsWith(".") ? zoneName : zoneName + "."; + public void createZone(String baseUrl, String apiKey, String zoneName, String nameServers) { + String normalizedZone = formatZoneName(zoneName); try { String url = buildApiUrl(baseUrl, "/servers/localhost/zones"); ObjectNode json = MAPPER.createObjectNode(); @@ -98,13 +104,15 @@ public void createZone(String baseUrl, String apiKey, String zoneName, List nsNames = new ArrayList<>(Arrays.asList(nameServers.split(","))); + if (!CollectionUtils.isEmpty(nsNames)) { + ArrayNode nsArray = json.putArray("nameservers"); + for (String ns : nsNames) { + nsArray.add(ns.endsWith(".") ? ns : ns + "."); + } } } - HttpPost request = new HttpPost(url); request.addHeader("X-API-Key", apiKey); request.addHeader("Content-Type", "application/json"); @@ -114,9 +122,7 @@ public void createZone(String baseUrl, String apiKey, String zoneName, List contents, String changeType) { + String normalizedZone = formatZoneName(zoneName); + String normalizedRecord = formatRecordName(recordName, zoneName); + + try { + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone); + + ObjectNode root = MAPPER.createObjectNode(); + ArrayNode rrsets = root.putArray("rrsets"); + ObjectNode rrset = rrsets.addObject(); + + rrset.put("name", normalizedRecord); + rrset.put("type", type.toUpperCase()); + rrset.put("ttl", ttl); + rrset.put("changetype", changeType); + + ArrayNode records = rrset.putArray("records"); + if (!CollectionUtils.isEmpty(contents)) { + for (String content : contents) { + ObjectNode record = records.addObject(); + record.put("content", content); + record.put("disabled", false); + } + } + + HttpPatch request = new HttpPatch(url); + request.addHeader("X-API-Key", apiKey); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Accept", "application/json"); + request.setEntity(new StringEntity(root.toString())); + + try (CloseableHttpResponse response = httpClient.execute(request)) { + int statusCode = response.getStatusLine().getStatusCode(); + String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; + + if (statusCode == HttpStatus.SC_NO_CONTENT) { + logger.debug("Record {} {} added/updated in zone {}", normalizedRecord, type, normalizedZone); + return; + } + + if (statusCode == HttpStatus.SC_NOT_FOUND) { + throw new CloudRuntimeException("Zone not found: " + normalizedZone); + } + + if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { + throw new CloudRuntimeException("Invalid PowerDNS API key"); + } + + logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body); + throw new CloudRuntimeException("Failed to add/update record " + normalizedRecord); + } + + } catch (IOException e) { + throw new CloudRuntimeException("Error while adding PowerDNS record", e); + } + } + + public void deleteRecord(String baseUrl, String apiKey, String zoneName, String recordName, String type) { + + String normalizedZone = formatZoneName(zoneName); + String normalizedRecord = formatRecordName(recordName, zoneName); + + try { + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone); + + ObjectNode root = MAPPER.createObjectNode(); + ArrayNode rrsets = root.putArray("rrsets"); + ObjectNode rrset = rrsets.addObject(); + + rrset.put("name", normalizedRecord); + rrset.put("type", type.toUpperCase()); + rrset.put("changetype", "DELETE"); + + HttpPatch request = new HttpPatch(url); + request.addHeader("X-API-Key", apiKey); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Accept", "application/json"); + request.setEntity(new StringEntity(root.toString())); + + try (CloseableHttpResponse response = httpClient.execute(request)) { + int statusCode = response.getStatusLine().getStatusCode(); + String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; + + if (statusCode == HttpStatus.SC_NO_CONTENT) { + logger.debug("Record {} {} deleted", normalizedRecord, type); + return; + } + + if (statusCode == HttpStatus.SC_NOT_FOUND) { + logger.debug("Record {} {} not found (idempotent delete)", normalizedRecord, type); + return; + } + + if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { + throw new CloudRuntimeException("Invalid PowerDNS API key"); + } + + logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body); + throw new CloudRuntimeException("Failed to delete record " + normalizedRecord); + } + + } catch (IOException e) { + throw new CloudRuntimeException("Error while deleting PowerDNS record", e); + } + } + + public Iterable listRecords(String baseUrl, String apiKey, String zoneName) { + String normalizedZone = formatZoneName(zoneName); + try { + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone); + + HttpGet request = new HttpGet(url); + request.addHeader("X-API-Key", apiKey); + request.addHeader("Accept", "application/json"); + + try (CloseableHttpResponse response = httpClient.execute(request)) { + + int statusCode = response.getStatusLine().getStatusCode(); + String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; + + if (statusCode == HttpStatus.SC_OK) { + JsonNode zone = MAPPER.readTree(body); + JsonNode rrsets = zone.path("rrsets"); + + if (rrsets.isArray()) { + return rrsets; + } + + return Collections.emptyList(); + } + + if (statusCode == HttpStatus.SC_NOT_FOUND) { + throw new CloudRuntimeException("Zone not found: " + normalizedZone); + } + + if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { + throw new CloudRuntimeException("Invalid PowerDNS API key"); + } + + throw new CloudRuntimeException("Failed to list records for zone " + normalizedZone); + } + + } catch (IOException e) { + throw new CloudRuntimeException("Error while listing PowerDNS records", e); + } + } public PowerDnsClient() { RequestConfig config = RequestConfig.custom() @@ -204,6 +359,42 @@ private String normalizeBaseUrl(String baseUrl) { return normalizedUrl; } + private String formatZoneName(String zoneName) { + String zone = zoneName.trim().toLowerCase(); + if (!zone.endsWith(".")) { + zone += "."; + } + return zone; + } + + private String formatRecordName(String recordName, String zoneName) { + if (recordName == null) { + throw new IllegalArgumentException("Record name cannot be null"); + } + String normalizedZone = formatZoneName(zoneName); + String zoneWithoutDot = normalizedZone.substring(0, normalizedZone.length() - 1); + + String name = recordName.trim().toLowerCase(); + + // Root record + if (name.equals("@") || name.isEmpty()) { + return normalizedZone; + } + + // Already absolute + if (name.endsWith(".")) { + return name; + } + + // Fully qualified but missing trailing dot + if (name.equals(zoneWithoutDot) || name.endsWith("." + zoneWithoutDot)) { + return name + "."; + } + + // Relative name + return name + "." + normalizedZone; + } + private String buildApiUrl(String baseUrl, String path) { return normalizeBaseUrl(baseUrl) + "/api/v1" + path; } 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 3e9b5d5f83de..cb4740fff9d8 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 @@ -17,6 +17,7 @@ package org.apache.cloudstack.dns.powerdns; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -28,6 +29,7 @@ import com.cloud.utils.StringUtils; import com.cloud.utils.component.AdapterBase; +import com.fasterxml.jackson.databind.JsonNode; public class PowerDnsProvider extends AdapterBase implements DnsProvider { @@ -46,7 +48,7 @@ public void validate(DnsServer server) { @Override public void provisionZone(DnsServer server, DnsZone zone) { validateServerZoneParams(server, zone); - client.createZone(server.getUrl(), server.getApiKey(), zone.getName(), null); + client.createZone(server.getUrl(), server.getApiKey(), zone.getName(), server.getNameServers()); } @Override @@ -55,24 +57,61 @@ public void deleteZone(DnsServer server, DnsZone zone) { client.deleteZone(server.getUrl(), server.getApiKey(), zone.getName()); } + public enum ChangeType { + REPLACE, DELETE + } + @Override - public DnsRecord createRecord(DnsServer server, DnsZone zone, DnsRecord record) { - return null; + public void addRecord(DnsServer server, DnsZone zone, DnsRecord record) { + validateServerZoneParams(server, zone); + applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.REPLACE); } @Override - public boolean updateRecord(DnsServer server, DnsZone zone, DnsRecord record) { - return false; + public void updateRecord(DnsServer server, DnsZone zone, DnsRecord record) { + addRecord(server, zone, record); } + @Override - public boolean deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) { - return false; + public void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) { + validateServerZoneParams(server, zone); + applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.DELETE); } + public void applyRecord(String serverUrl, String apiKey, String zoneName, DnsRecord record, ChangeType changeType) { + client.modifyRecord(serverUrl, apiKey, zoneName, record.getName(), record.getType().name(), + record.getTtl(), record.getContents(), changeType.name()); + } + + + @Override public List listRecords(DnsServer server, DnsZone zone) { - return List.of(); + List records = new ArrayList<>(); + for (JsonNode rrset: client.listRecords(server.getUrl(), server.getApiKey(), zone.getName())) { + String name = rrset.path("name").asText(); + String typeStr = rrset.path("type").asText(); + int ttl = rrset.path("ttl").asInt(0); + if (!"SOA".equalsIgnoreCase(typeStr)) { + try { + List contents = new ArrayList<>(); + JsonNode recordsNode = rrset.path("records"); + if (recordsNode.isArray()) { + for (JsonNode rec : recordsNode) { + String content = rec.path("content").asText(); + if (!content.isEmpty()) { + contents.add(content); + } + } + } + records.add(new DnsRecord(name, DnsRecord.RecordType.valueOf(typeStr), contents, ttl)); + } catch (Exception ignored) { + // Skip unsupported record types + } + } + } + return records; } void validateServerZoneParams(DnsServer server, DnsZone zone) { 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 6cc1b62e13d0..c4f7c6155bfa 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -181,6 +181,10 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { if (cmd.getNameServers() != null) { dnsServer.setNameServers(cmd.getNameServers()); } + if (cmd.getState() != null) { + dnsServer.setState(cmd.getState()); + } + if (validationRequired) { DnsProvider provider = getProvider(dnsServer.getProviderType()); try { @@ -190,6 +194,7 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { throw new InvalidParameterValueException("Validation failed for DNS server"); } } + boolean updateStatus = dnsServerDao.update(dnsServerId, dnsServer); if (updateStatus) { @@ -308,17 +313,79 @@ public DnsZone getDnsZone(long id) { @Override public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { - return null; + DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); + if (zone == 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()); + try { + DnsRecord record = new DnsRecord(cmd.getName(), cmd.getType(), cmd.getContents(), cmd.getTtl()); + DnsProvider provider = getProvider(server.getProviderType()); + // Add Record via Provider + provider.addRecord(server, zone, record); + return createDnsRecordResponse(record); + } catch (Exception ex) { + logger.error("Failed to add DNS record via provider", ex); + throw new CloudRuntimeException(String.format("Failed to add DNS record: %s", cmd.getName())); + } } @Override public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) { - return false; + DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); + if (zone == 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()); + + try { + // Reconstruct the record DTO just for deletion criteria + DnsRecord record = new DnsRecord(); + record.setName(cmd.getName()); + record.setType(cmd.getType()); + DnsProvider provider = getProvider(server.getProviderType()); + provider.deleteRecord(server, zone, record); + return true; + } catch (Exception ex) { + logger.error("Failed to delete DNS record via provider", ex); + throw new CloudRuntimeException(String.format("Failed to delete record: %s", cmd.getName())); + } } @Override public ListResponse listDnsRecords(ListDnsRecordsCmd cmd) { - return null; + DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); + if (zone == null) { + throw new InvalidParameterValueException(String.format("DNS Zone with ID %s not found.", cmd.getDnsZoneId())); + } + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, true, zone); + DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); + if (server == null) { + throw new CloudRuntimeException("The underlying DNS Server for this zone is missing."); + } + try { + DnsProvider provider = getProvider(server.getProviderType()); + List records = provider.listRecords(server, zone); + List responses = new ArrayList<>(); + for (DnsRecord record : records) { + responses.add(createDnsRecordResponse(record)); + } + + ListResponse listResponse = new ListResponse<>(); + listResponse.setResponses(responses, responses.size()); + return listResponse; + } catch (Exception ex) { + logger.error("Failed to list DNS records from provider", ex); + throw new CloudRuntimeException("Failed to fetch DNS records"); + } } @Override @@ -393,6 +460,15 @@ public DnsZoneResponse createDnsZoneResponse(DnsZone zone) { return res; } + @Override + public DnsRecordResponse createDnsRecordResponse(DnsRecord record) { + DnsRecordResponse res = new DnsRecordResponse(); + res.setName(record.getName()); + res.setType(record.getType()); + res.setContent(record.getContents()); + return res; + } + @Override public boolean start() { if (dnsProviders == null || dnsProviders.isEmpty()) { diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java index 4e216f8fac48..a200cb988923 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.dns.vo; import java.util.Date; +import java.util.List; import java.util.UUID; import javax.persistence.Column; @@ -95,7 +96,7 @@ public class DnsServerVO implements DnsServer { } public DnsServerVO(String name, String url, DnsProviderType providerType, String apiKey, - Integer port, boolean isPublic, String publicDomainSuffix, String nameServers, + Integer port, boolean isPublic, String publicDomainSuffix, List nameServers, long accountId) { this(); this.name = name; @@ -105,9 +106,9 @@ public DnsServerVO(String name, String url, DnsProviderType providerType, String this.apiKey = apiKey; this.accountId = accountId; this.publicDomainSuffix = publicDomainSuffix; - this.nameServers = nameServers; this.isPublic = isPublic; this.state = State.Enabled; + this.nameServers = String.join(",", nameServers);; } @Override @@ -204,4 +205,8 @@ public void setUrl(String url) { public void setName(String name) { this.name = name; } + + public String getNameServers() { + return nameServers; + } } From c5972aea4e440596c2e5b5f60b94b9351ce7f8f9 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Mon, 16 Feb 2026 18:38:48 +0530 Subject: [PATCH 08/23] Add associate and disassociate zone to network APIs --- .../dns/AssociateDnsZoneToNetworkCmd.java | 79 +++++++++++++++++++ .../command/user/dns/CreateDnsZoneCmd.java | 1 - .../DisassociateDnsZoneFromNetworkCmd.java | 65 +++++++++++++++ .../response/DnsZoneNetworkMapResponse.java | 64 +++++++++++++++ .../cloudstack/dns/DnsProviderManager.java | 8 +- .../META-INF/db/schema-42210to42300.sql | 4 + .../dns/DnsProviderManagerImpl.java | 57 +++++++++++++ .../dns/dao/DnsZoneNetworkMapDao.java | 2 + .../dns/dao/DnsZoneNetworkMapDaoImpl.java | 10 +++ .../dns/vo/DnsZoneNetworkMapVO.java | 9 +++ 10 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/DnsZoneNetworkMapResponse.java diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java new file mode 100644 index 000000000000..e296bb45f28e --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.NetworkResponse; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; + +@APICommand(name = "associateDnsZoneToNetwork", description = "Associates a DNS Zone with a Network for VM auto-registration", + responseObject = DnsZoneNetworkMapResponse.class, requestHasSensitiveInfo = false) +public class AssociateDnsZoneToNetworkCmd extends BaseCmd { + + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "The ID of the DNS zone") + private Long dnsZoneId; + + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, + required = true, description = "The ID of the network") + private Long networkId; + + @Parameter(name = "subdomain", type = CommandType.STRING, + description = "Optional subdomain to append (e.g., 'dev' creates vm1.dev.example.com)") + private String subDomain; + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + DnsZoneNetworkMapResponse response = dnsProviderManager.associateZoneToNetwork(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return dnsProviderManager.getDnsZone(dnsZoneId).getAccountId(); + } + + public Long getDnsZoneId() { + return dnsZoneId; + } + + public Long getNetworkId() { + return networkId; + } + + public String getSubDomain() { + return subDomain; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java index 3e85946222c8..5227db4099df 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java @@ -22,7 +22,6 @@ responseObject = DnsZoneResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { - private static final String COMMAND_RESPONSE_NAME = "creatednszoneresponse"; @Inject diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java new file mode 100644 index 000000000000..ddbe72326f31 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java @@ -0,0 +1,65 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; + +@APICommand(name = "disassociateDnsZoneFromNetwork", description = "Removes the association between a DNS Zone and a Network", + responseObject = SuccessResponse.class) +public class DisassociateDnsZoneFromNetworkCmd extends BaseCmd { + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneNetworkMapResponse.class, + required = true, description = "The ID of the DNS zone to network mapping") + private Long id; + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = dnsProviderManager.disassociateZoneFromNetwork(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to disassociate DNS zone from network."); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getId() { return id; } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneNetworkMapResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneNetworkMapResponse.java new file mode 100644 index 000000000000..84fe46ce5aca --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneNetworkMapResponse.java @@ -0,0 +1,64 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class DnsZoneNetworkMapResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "The ID of the mapping") + private String id; + + @SerializedName(ApiConstants.DNS_ZONE_ID) + @Param(description = "The ID of the DNS zone") + private String dnsZoneId; + + @SerializedName(ApiConstants.NETWORK_ID) + @Param(description = "The ID of the Network") + private String networkId; + + @SerializedName("subdomain") + @Param(description = "The sub domain name of the auto-registered DNS record") + private String subDomain; + + public DnsZoneNetworkMapResponse() { + super(); + setObjectName("dnszonenetwork"); + } + + // Setters + public void setId(String id) { + this.id = id; + } + + public void setDnsZoneId(String dnsZoneId) { + this.dnsZoneId = dnsZoneId; + } + + public void setNetworkId(String networkId) { + this.networkId = networkId; + } + + public void setSubDomain(String subDomain) { + this.subDomain = subDomain; + } +} 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 b918b49fb39e..00950fca3fa1 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -20,10 +20,12 @@ import java.util.List; import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd; import org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd; import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; import org.apache.cloudstack.api.command.user.dns.DeleteDnsRecordCmd; import org.apache.cloudstack.api.command.user.dns.DeleteDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.DisassociateDnsZoneFromNetworkCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; @@ -31,6 +33,7 @@ import org.apache.cloudstack.api.command.user.dns.UpdateDnsZoneCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; @@ -64,8 +67,11 @@ public interface DnsProviderManager extends Manager, PluggableService { List listProviderNames(); - // Helper to create the response object DnsZoneResponse createDnsZoneResponse(DnsZone zone); DnsRecordResponse createDnsRecordResponse(DnsRecord record); + + DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetworkCmd cmd); + + boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd); } 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 e0731301bbce..f34bb1894709 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 @@ -102,10 +102,14 @@ CREATE TABLE `cloud`.`dns_zone` ( -- 3. DNS Zone Network Map (One-to-Many Link) CREATE TABLE `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, `network_id` bigint(20) unsigned NOT NULL COMMENT 'network to which dns zone is associated to', `sub_domain` varchar(255) DEFAULT NULL COMMENT 'Subdomain for auto-registration', + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', PRIMARY KEY (`id`), + CONSTRAINT `uc_dns_zone__uuid` UNIQUE (`uuid`), KEY `fk_dns_map__zone_id` (`dns_zone_id`), KEY `fk_dns_map__network_id` (`network_id`), CONSTRAINT `fk_dns_map__zone_id` FOREIGN KEY (`dns_zone_id`) REFERENCES `dns_zone` (`id`) ON DELETE CASCADE, 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 c4f7c6155bfa..5eb430bb3fb5 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -24,11 +24,13 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd; import org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd; import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; import org.apache.cloudstack.api.command.user.dns.DeleteDnsRecordCmd; import org.apache.cloudstack.api.command.user.dns.DeleteDnsServerCmd; import org.apache.cloudstack.api.command.user.dns.DeleteDnsZoneCmd; +import org.apache.cloudstack.api.command.user.dns.DisassociateDnsZoneFromNetworkCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsProvidersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; @@ -37,17 +39,22 @@ import org.apache.cloudstack.api.command.user.dns.UpdateDnsZoneCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; import org.apache.cloudstack.api.response.DnsServerResponse; +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.DnsServerDao; import org.apache.cloudstack.dns.dao.DnsZoneDao; +import org.apache.cloudstack.dns.dao.DnsZoneNetworkMapDao; import org.apache.cloudstack.dns.vo.DnsServerVO; +import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; import org.apache.cloudstack.dns.vo.DnsZoneVO; import org.springframework.stereotype.Component; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; +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.utils.Pair; @@ -65,6 +72,10 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa DnsServerDao dnsServerDao; @Inject DnsZoneDao dnsZoneDao; + @Inject + NetworkDao networkDao; + @Inject + DnsZoneNetworkMapDao dnsZoneNetworkMapDao; private DnsProvider getProvider(DnsProviderType type) { if (type == null) { @@ -469,6 +480,51 @@ public DnsRecordResponse createDnsRecordResponse(DnsRecord record) { return res; } + @Override + public DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetworkCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); + DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); + if (zone == null) { + throw new InvalidParameterValueException("DNS Zone not found."); + } + accountMgr.checkAccess(caller, null, true, zone); + + NetworkVO network = networkDao.findById(cmd.getNetworkId()); + if (network == null) { + throw new InvalidParameterValueException("Network not found."); + } + accountMgr.checkAccess(caller, null, true, network); + DnsZoneNetworkMapVO existing = dnsZoneNetworkMapDao.findByZoneAndNetwork(zone.getId(), network.getId()); + if (existing != null) { + throw new InvalidParameterValueException("This DNS Zone is already associated with this Network."); + } + DnsZoneNetworkMapVO mapping = new DnsZoneNetworkMapVO(zone.getId(), network.getId(), cmd.getSubDomain()); + dnsZoneNetworkMapDao.persist(mapping); + DnsZoneNetworkMapResponse response = new DnsZoneNetworkMapResponse(); + response.setId(mapping.getUuid()); + response.setDnsZoneId(zone.getUuid()); + response.setNetworkId(network.getUuid()); + response.setSubDomain(mapping.getSubDomain()); + return response; + } + + @Override + public boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd) { + DnsZoneNetworkMapVO mapping = dnsZoneNetworkMapDao.findById(cmd.getId()); + if (mapping == null) { + throw new InvalidParameterValueException("The specified DNS zone to network mapping does not exist."); + } + DnsZoneVO zone = dnsZoneDao.findById(mapping.getDnsZoneId()); + if (zone == null) { + // If the zone is missing but the mapping exists (shouldn't happen due to CASCADE DELETE), + // clean up the orphaned mapping. + return dnsZoneNetworkMapDao.remove(mapping.getId()); + } + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, true, zone); + return dnsZoneNetworkMapDao.remove(mapping.getId()); + } + @Override public boolean start() { if (dnsProviders == null || dnsProviders.isEmpty()) { @@ -495,6 +551,7 @@ public List> getCommands() { cmdList.add(ListDnsZonesCmd.class); cmdList.add(DeleteDnsZoneCmd.class); cmdList.add(UpdateDnsZoneCmd.class); + cmdList.add(AssociateDnsZoneToNetworkCmd.class); // DNS Record Commands cmdList.add(CreateDnsRecordCmd.class); diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java index 9d79a0e8708d..5b0309d16790 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java @@ -8,4 +8,6 @@ public interface DnsZoneNetworkMapDao extends GenericDao { List listByDnsZoneId(long dnsZoneId); + DnsZoneNetworkMapVO findByZoneAndNetwork(long zoneId, long networkId); + List listByNetworkId(long networkId); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java index adf1a09111f0..dc21c953f974 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java @@ -25,4 +25,14 @@ public List listByDnsZoneId(long dnsZoneId) { sc.setParameters("dnsZoneId", dnsZoneId); return listBy(sc); } + + @Override + public DnsZoneNetworkMapVO findByZoneAndNetwork(long zoneId, long networkId) { + return null; + } + + @Override + public List listByNetworkId(long networkId) { + return List.of(); + } } diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java index 9500240d81a1..a94e12549209 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.dns.vo; import java.util.Date; +import java.util.UUID; import javax.persistence.Column; import javax.persistence.Entity; @@ -41,6 +42,9 @@ public class DnsZoneNetworkMapVO implements InternalIdentity { @Column(name = "id") private long id; + @Column(name = "uuid") + private String uuid; + @Column(name = "dns_zone_id") private long dnsZoneId; @@ -59,6 +63,7 @@ public class DnsZoneNetworkMapVO implements InternalIdentity { private Date removed = null; public DnsZoneNetworkMapVO() { + this.uuid = UUID.randomUUID().toString(); this.created = new Date(); } @@ -93,4 +98,8 @@ public Date getCreated() { public Date getRemoved() { return removed; } + + public String getUuid() { + return uuid; + } } From 99f8c7dad8cf06e4f923e9b27dfafa97a30d225d Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 17 Feb 2026 17:21:26 +0530 Subject: [PATCH 09/23] following things are done: 1. Registerdnsrecordforvm api 2. removednsrecordforvm api 3. cleanup; fixed license, dao logic --- .../com/cloud/configuration/Resource.java | 3 +- .../com/cloud/user/ResourceLimitService.java | 2 + .../api/command/user/dns/AddDnsServerCmd.java | 4 +- .../dns/AssociateDnsZoneToNetworkCmd.java | 3 +- .../command/user/dns/CreateDnsRecordCmd.java | 20 +++- .../command/user/dns/CreateDnsZoneCmd.java | 27 +++-- .../command/user/dns/DeleteDnsRecordCmd.java | 20 +++- .../command/user/dns/DeleteDnsServerCmd.java | 27 +++-- .../command/user/dns/DeleteDnsZoneCmd.java | 27 +++-- .../DisassociateDnsZoneFromNetworkCmd.java | 2 +- .../command/user/dns/ListDnsProvidersCmd.java | 3 +- .../command/user/dns/ListDnsRecordsCmd.java | 21 +++- .../command/user/dns/ListDnsServersCmd.java | 27 +++-- .../api/command/user/dns/ListDnsZonesCmd.java | 28 +++-- .../user/dns/RegisterDnsRecordForVmCmd.java | 74 ++++++++++++ .../user/dns/RemoveDnsRecordForVmCmd.java | 79 ++++++++++++ .../command/user/dns/UpdateDnsServerCmd.java | 4 +- .../command/user/dns/UpdateDnsZoneCmd.java | 29 +++-- .../cloudstack/dns/DnsProviderManager.java | 5 + .../META-INF/db/schema-42210to42300.sql | 10 +- .../ResourceLimitManagerImpl.java | 1 + .../dns/DnsProviderManagerImpl.java | 113 ++++++++++++++++++ .../dns/dao/DnsZoneNetworkMapDao.java | 2 +- .../dns/dao/DnsZoneNetworkMapDaoImpl.java | 30 ++++- 24 files changed, 488 insertions(+), 73 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java diff --git a/api/src/main/java/com/cloud/configuration/Resource.java b/api/src/main/java/com/cloud/configuration/Resource.java index 97be7f9d64c5..41abce02eb47 100644 --- a/api/src/main/java/com/cloud/configuration/Resource.java +++ b/api/src/main/java/com/cloud/configuration/Resource.java @@ -38,7 +38,8 @@ enum ResourceType { // All storage type resources are allocated_storage and not backup_storage("backup_storage", 13), bucket("bucket", 14), object_storage("object_storage", 15), - gpu("gpu", 16); + gpu("gpu", 16), + dns_zone("dns_zone", 17); private String name; private int ordinal; diff --git a/api/src/main/java/com/cloud/user/ResourceLimitService.java b/api/src/main/java/com/cloud/user/ResourceLimitService.java index 738e593582b4..0622c5eb0806 100644 --- a/api/src/main/java/com/cloud/user/ResourceLimitService.java +++ b/api/src/main/java/com/cloud/user/ResourceLimitService.java @@ -56,6 +56,8 @@ public interface ResourceLimitService { "The default maximum number of GPU devices that can be used for a domain", false); static final ConfigKey DefaultMaxProjectGpus = new ConfigKey<>("Project Defaults",Long.class,"max.project.gpus","20", "The default maximum number of GPU devices that can be used for a project", false); + ConfigKey DefaultMaxDnsAccounts = new ConfigKey<>("Account Defaults",Long.class, "max.account.dns_zones","10", + "The default maximum number of DNS zones that can be created by an Account", true); static final List HostTagsSupportingTypes = List.of(ResourceType.user_vm, ResourceType.cpu, ResourceType.memory, ResourceType.gpu); static final List StorageTagsSupportingTypes = List.of(ResourceType.volume, ResourceType.primary_storage); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java index e5e38b7cbda5..126aafba6420 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -33,8 +33,8 @@ import org.apache.cloudstack.dns.DnsServer; import org.apache.commons.lang3.BooleanUtils; -@APICommand(name = "addDnsServer", description = "Adds a new external DNS server", - responseObject = DnsServerResponse.class, requestHasSensitiveInfo = true) +@APICommand(name = "addDnsServer", description = "Adds a new external DNS server", responseObject = DnsServerResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0") public class AddDnsServerCmd extends BaseCmd { @Inject diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java index e296bb45f28e..b8cdbd703451 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java @@ -34,7 +34,8 @@ import com.cloud.exception.ResourceUnavailableException; @APICommand(name = "associateDnsZoneToNetwork", description = "Associates a DNS Zone with a Network for VM auto-registration", - responseObject = DnsZoneNetworkMapResponse.class, requestHasSensitiveInfo = false) + responseObject = DnsZoneNetworkMapResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, since = "4.23.0") public class AssociateDnsZoneToNetworkCmd extends BaseCmd { @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java index 2967fabb9f0c..077e98c23244 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package org.apache.cloudstack.api.command.user.dns; import java.util.List; @@ -18,7 +35,8 @@ import com.cloud.utils.EnumUtils; @APICommand(name = "createDnsRecord", description = "Creates a DNS record directly on the provider", - responseObject = DnsRecordResponse.class) + responseObject = DnsRecordResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0") public class CreateDnsRecordCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java index 5227db4099df..467b0c787969 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package org.apache.cloudstack.api.command.user.dns; import javax.inject.Inject; @@ -19,10 +36,9 @@ import com.cloud.exception.ResourceAllocationException; @APICommand(name = "createDnsZone", description = "Creates a new DNS Zone on a specific server", - responseObject = DnsZoneResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) + responseObject = DnsZoneResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, since = "4.23.0") public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { - private static final String COMMAND_RESPONSE_NAME = "creatednszoneresponse"; @Inject DnsProviderManager dnsProviderManager; @@ -109,11 +125,6 @@ public void execute() { } } - @Override - public String getCommandName() { - return COMMAND_RESPONSE_NAME; - } - @Override public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java index 79e688db1ca1..3c9207e7dc74 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.api.APICommand; @@ -16,7 +33,8 @@ import com.cloud.utils.EnumUtils; @APICommand(name = "deleteDnsRecord", description = "Deletes a DNS record from the external provider", - responseObject = SuccessResponse.class) + responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, since = "4.23.0") public class DeleteDnsRecordCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java index 155aa2006317..bc6b1f23d053 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.api.APICommand; @@ -14,10 +31,9 @@ import com.cloud.user.Account; @APICommand(name = "deleteDnsServer", description = "Removes a DNS server integration", - responseObject = SuccessResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) + responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, since = "4.23.0") public class DeleteDnsServerCmd extends BaseAsyncCmd { - private static final String COMMAND_RESPONSE_NAME = "deletednsserverresponse"; ///////////////////////////////////////////////////// //////////////// API Parameters ///////////////////// @@ -54,11 +70,6 @@ public void execute() { } } - @Override - public String getCommandName() { - return COMMAND_RESPONSE_NAME; - } - @Override public long getEntityOwnerId() { // Look up the server to find its owner. diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java index 6b17f5249f47..b4c5f780a685 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.api.APICommand; @@ -14,10 +31,9 @@ import com.cloud.user.Account; @APICommand(name = "deleteDnsZone", description = "Removes a DNS Zone from CloudStack and the external provider", - responseObject = SuccessResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) + responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0") public class DeleteDnsZoneCmd extends BaseAsyncCmd { - private static final String COMMAND_RESPONSE_NAME = "deletednszoneresponse"; ///////////////////////////////////////////////////// //////////////// API Parameters ///////////////////// @@ -53,11 +69,6 @@ public void execute() { } } - @Override - public String getCommandName() { - return COMMAND_RESPONSE_NAME; - } - @Override public long getEntityOwnerId() { // Look up the Zone to find the Account Owner diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java index ddbe72326f31..c8f8ca7a86b1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java @@ -34,7 +34,7 @@ import com.cloud.user.Account; @APICommand(name = "disassociateDnsZoneFromNetwork", description = "Removes the association between a DNS Zone and a Network", - responseObject = SuccessResponse.class) + responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0") public class DisassociateDnsZoneFromNetworkCmd extends BaseCmd { @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneNetworkMapResponse.class, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java index 1be4d068e1de..7d808ad2c88f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java @@ -29,7 +29,8 @@ import org.apache.cloudstack.dns.DnsProviderManager; @APICommand(name = "listDnsProviders", description = "Lists available DNS plugin providers", - responseObject = DnsProviderResponse.class, requestHasSensitiveInfo = false) + responseObject = DnsProviderResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, since = "4.23.0") public class ListDnsProvidersCmd extends BaseListCmd { @Inject diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java index c419ef98cc1e..80d08182f1d0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.api.APICommand; @@ -9,8 +26,8 @@ import org.apache.cloudstack.api.response.ListResponse; @APICommand(name = "listDnsRecords", description = "Lists DNS records from the external provider", - responseObject = DnsRecordResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) + responseObject = DnsRecordResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, since = "4.23.0") public class ListDnsRecordsCmd extends BaseListCmd { @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java index cd408dcb6adb..fad8cddd32cc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package org.apache.cloudstack.api.command.user.dns; import javax.inject.Inject; @@ -11,10 +28,9 @@ import org.apache.cloudstack.dns.DnsProviderManager; @APICommand(name = "listDnsServers", description = "Lists DNS servers owned by the account.", - responseObject = DnsServerResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) + responseObject = DnsServerResponse.class, requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, since = "4.23.0") public class ListDnsServersCmd extends BaseListAccountResourcesCmd { - private static final String COMMAND_RESPONSE_NAME = "listdnsserversresponse"; @Inject DnsProviderManager dnsProviderManager; @@ -54,9 +70,4 @@ public void execute() { response.setObjectName("dnsserver"); setResponseObject(response); } - - @Override - public String getCommandName() { - return COMMAND_RESPONSE_NAME; - } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java index edc65eef9262..6ea329dee8b6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.api.APICommand; @@ -8,11 +25,9 @@ import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; -@APICommand(name = "listDnsZones", description = "Lists DNS zones.", - responseObject = DnsZoneResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +@APICommand(name = "listDnsZones", description = "Lists DNS zones.", responseObject = DnsZoneResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0") public class ListDnsZonesCmd extends BaseListAccountResourcesCmd { - private static final String COMMAND_RESPONSE_NAME = "listdnszonesresponse"; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -39,9 +54,4 @@ public void execute() { response.setResponseName(getCommandName()); setResponseObject(response); } - - @Override - public String getCommandName() { - return COMMAND_RESPONSE_NAME; - } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java new file mode 100644 index 000000000000..6e6bd18f4701 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java @@ -0,0 +1,74 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.NetworkResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; + +@APICommand(name = "registerDnsRecordForVm", description = "Automatically registers a DNS record for a VM based on its associated Network and DNS Zone mapping", + responseObject = SuccessResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.DomainAdmin, RoleType.User}) +public class RegisterDnsRecordForVmCmd extends BaseCmd { + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, + required = true, description = "The ID of the Virtual Machine") + private Long vmId; + + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, + description = "The ID of the network. If not specified, the VM's default NIC network is used.") + private Long networkId; + + public Long getVmId() { return vmId; } + public Long getNetworkId() { return networkId; } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = dnsProviderManager.registerDnsRecordForVm(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to register DNS record for VM"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java new file mode 100644 index 000000000000..01a13168a794 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.user.dns; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.NetworkResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; + +@APICommand(name = "removeDnsRecordForVm", description = "Removes the auto-registered DNS record for a VM", + responseObject = SuccessResponse.class, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.DomainAdmin, RoleType.User}) +public class RemoveDnsRecordForVmCmd extends BaseCmd { + + @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, + required = true, description = "The ID of the Virtual Machine") + private Long vmId; + + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, + description = "The ID of the network. If not specified, the VM's default NIC network is used.") + private Long networkId; + + public Long getVmId() { + return vmId; + } + + public Long getNetworkId() { + return networkId; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = dnsProviderManager.removeDnsRecordForVm(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to remove DNS record for VM"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccountId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java index 051acc286007..ccd4cdc68777 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java @@ -34,8 +34,8 @@ import com.cloud.utils.EnumUtils; -@APICommand(name = "updateDnsServer", description = "Update DNS server", - responseObject = DnsServerResponse.class, requestHasSensitiveInfo = true) +@APICommand(name = "updateDnsServer", description = "Update DNS server", responseObject = DnsServerResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0") public class UpdateDnsServerCmd extends BaseCmd { @Inject diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java index d679db706461..1c191a94509e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + package org.apache.cloudstack.api.command.user.dns; import javax.inject.Inject; @@ -13,13 +30,10 @@ import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.dns.DnsZone; -@APICommand(name = "updateDnsZone", description = "Updates a DNS Zone's metadata", - responseObject = DnsZoneResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +@APICommand(name = "updateDnsZone", description = "Updates a DNS Zone's metadata", responseObject = DnsZoneResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0") public class UpdateDnsZoneCmd extends BaseCmd { - private static final String COMMAND_RESPONSE_NAME = "updatednszoneresponse"; - @Inject DnsProviderManager dnsProviderManager; @@ -66,11 +80,6 @@ public void execute() { } } - @Override - public String getCommandName() { - return COMMAND_RESPONSE_NAME; - } - @Override public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); 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 00950fca3fa1..e3b7431803fb 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -29,6 +29,8 @@ import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; +import org.apache.cloudstack.api.command.user.dns.RegisterDnsRecordForVmCmd; +import org.apache.cloudstack.api.command.user.dns.RemoveDnsRecordForVmCmd; import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; import org.apache.cloudstack.api.command.user.dns.UpdateDnsZoneCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; @@ -74,4 +76,7 @@ public interface DnsProviderManager extends Manager, PluggableService { DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetworkCmd cmd); boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd); + + boolean registerDnsRecordForVm(RegisterDnsRecordForVmCmd cmd); + boolean removeDnsRecordForVm(RemoveDnsRecordForVmCmd cmd); } 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 f34bb1894709..06054c7cce8e 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 @@ -55,7 +55,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` ( -- DNS Framework Schema -- ====================================================================== --- 1. DNS Server Table (Stores DNS Server Configurations) +-- DNS Server Table (Stores DNS Server Configurations) CREATE TABLE `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', @@ -76,7 +76,7 @@ CREATE TABLE `cloud`.`dns_server` ( CONSTRAINT `fk_dns_server__account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- 2. DNS Zone Table (Stores DNS Zone Metadata) +-- DNS Zone Table (Stores DNS Zone Metadata) CREATE TABLE `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', @@ -99,7 +99,7 @@ CREATE TABLE `cloud`.`dns_zone` ( CONSTRAINT `fk_dns_zone__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- 3. DNS Zone Network Map (One-to-Many Link) +-- DNS Zone Network Map (One-to-Many Link) CREATE TABLE `cloud`.`dns_zone_network_map` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone to network mapping', `uuid` varchar(40), @@ -115,3 +115,7 @@ CREATE TABLE `cloud`.`dns_zone_network_map` ( CONSTRAINT `fk_dns_map__zone_id` FOREIGN KEY (`dns_zone_id`) REFERENCES `dns_zone` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_dns_map__network_id` FOREIGN KEY (`network_id`) REFERENCES `networks` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Set default limit to 10 DNS zones for standard Accounts +INSERT INTO `cloud`.`configuration` (`category`, `instance`, `component`, `name`, `value`, `description`, `default_value`) +VALUES ('Advanced', 'DEFAULT', 'ResourceLimitManager', 'max.account.dns_zones', '10', 'The default maximum number of DNS zones that can be created by an Account', '10'); diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 0da403c35f2f..e12e66dc984d 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -322,6 +322,7 @@ public boolean configure(final String name, final Map params) th accountResourceLimitMap.put(Resource.ResourceType.backup_storage.name(), Long.parseLong(_configDao.getValue(BackupManager.DefaultMaxAccountBackupStorage.key()))); accountResourceLimitMap.put(Resource.ResourceType.bucket.name(), Long.parseLong(_configDao.getValue(BucketApiService.DefaultMaxAccountBuckets.key()))); accountResourceLimitMap.put(Resource.ResourceType.object_storage.name(), Long.parseLong(_configDao.getValue(BucketApiService.DefaultMaxAccountObjectStorage.key()))); + accountResourceLimitMap.put(ResourceType.dns_zone.name(), DefaultMaxDnsAccounts.value()); domainResourceLimitMap.put(Resource.ResourceType.public_ip.name(), Long.parseLong(_configDao.getValue(Config.DefaultMaxDomainPublicIPs.key()))); domainResourceLimitMap.put(Resource.ResourceType.snapshot.name(), Long.parseLong(_configDao.getValue(Config.DefaultMaxDomainSnapshots.key()))); 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 5eb430bb3fb5..740a5c31756b 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -35,6 +35,8 @@ import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; +import org.apache.cloudstack.api.command.user.dns.RegisterDnsRecordForVmCmd; +import org.apache.cloudstack.api.command.user.dns.RemoveDnsRecordForVmCmd; import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; import org.apache.cloudstack.api.command.user.dns.UpdateDnsZoneCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; @@ -62,6 +64,10 @@ import com.cloud.utils.component.PluggableService; import com.cloud.utils.db.Filter; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.NicVO; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; @Component public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderManager, PluggableService { @@ -76,6 +82,10 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa NetworkDao networkDao; @Inject DnsZoneNetworkMapDao dnsZoneNetworkMapDao; + @Inject + UserVmDao userVmDao; + @Inject + NicDao nicDao; private DnsProvider getProvider(DnsProviderType type) { if (type == null) { @@ -493,6 +503,10 @@ public DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetwor if (network == null) { throw new InvalidParameterValueException("Network not found."); } + + if (!NetworkVO.GuestType.Shared.equals(network.getGuestType())) { + throw new CloudRuntimeException(String.format("Operation is not permitted for network type: %s", network.getGuestType())); + } accountMgr.checkAccess(caller, null, true, network); DnsZoneNetworkMapVO existing = dnsZoneNetworkMapDao.findByZoneAndNetwork(zone.getId(), network.getId()); if (existing != null) { @@ -525,6 +539,103 @@ public boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd return dnsZoneNetworkMapDao.remove(mapping.getId()); } + @Override + public boolean registerDnsRecordForVm(RegisterDnsRecordForVmCmd cmd) { + return processDnsRecordForInstance(cmd.getVmId(), cmd.getNetworkId(), true); + } + + @Override + public boolean removeDnsRecordForVm(RemoveDnsRecordForVmCmd cmd) { + return processDnsRecordForInstance(cmd.getVmId(), cmd.getNetworkId(), false); + } + + /** + * Helper method to handle both Register and Remove logic for Instance + */ + private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boolean isAdd) { + // 1. Fetch VM and verify access + UserVmVO instance = userVmDao.findById(instanceId); + if (instance == null) { + throw new InvalidParameterValueException("Instance not found."); + } + accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, instance); + + // 2. Resolve the NIC and Network + NicVO nic; + if (networkId != null) { + nic = nicDao.findByNtwkIdAndInstanceId(networkId, instance.getId()); + } else { + nic = nicDao.findDefaultNicForVM(instance.getId()); + networkId = nic != null ? nic.getNetworkId() : null; + } + + if (nic == null) { + throw new CloudRuntimeException("No valid NIC found for this Instance on the specified Network."); + } + + // 3. Find if this network is linked to any DNS Zones + List mappings = dnsZoneNetworkMapDao.listByNetworkId(networkId); + if (mappings == null || mappings.isEmpty()) { + throw new CloudRuntimeException("No DNS zones are mapped to this network. Please associate a zone first."); + } + + boolean atLeastOneSuccess = false; + // 4. Iterate over mapped zones and push the record + for (DnsZoneNetworkMapVO map : mappings) { + DnsZoneVO zone = dnsZoneDao.findById(map.getDnsZoneId()); + if (zone == null || zone.getState() != DnsZone.State.Active) { + continue; + } + + DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); + + // Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain") + String recordName = String.valueOf(instance.getInstanceName()); + if (map.getSubDomain() != null && !map.getSubDomain().isEmpty()) { + recordName = recordName + "." + map.getSubDomain(); + } + + try { + DnsProvider provider = getProvider(server.getProviderType()); + // Handle IPv4 (A Record) + if (nic.getIPv4Address() != null) { + DnsRecord recordA = new DnsRecord(recordName, DnsRecord.RecordType.A, java.util.Arrays.asList(nic.getIPv4Address()), 3600); + if (isAdd) { + provider.addRecord(server, zone, recordA); + } else { + provider.deleteRecord(server, zone, recordA); + } + atLeastOneSuccess = true; + } + + // Handle IPv6 (AAAA Record) if it exists + if (nic.getIPv6Address() != null) { + DnsRecord recordAAAA = new DnsRecord(recordName, DnsRecord.RecordType.AAAA, java.util.Arrays.asList(nic.getIPv6Address()), 3600); + if (isAdd) { + provider.addRecord(server, zone, recordAAAA); + } else { + provider.deleteRecord(server, zone, recordAAAA); + } + atLeastOneSuccess = true; + } + + } catch (Exception ex) { + logger.error( + "Failed to {} DNS record for Instance {} in zone {}", + isAdd ? "register" : "remove", + instance.getHostName(), + zone.getName(), + ex + ); + } + } + + if (!atLeastOneSuccess) { + throw new CloudRuntimeException("Failed to process DNS records. Ensure the Instance has a valid IP address."); + } + return true; + } + @Override public boolean start() { if (dnsProviders == null || dnsProviders.isEmpty()) { @@ -557,6 +668,8 @@ public List> getCommands() { cmdList.add(CreateDnsRecordCmd.class); cmdList.add(ListDnsRecordsCmd.class); cmdList.add(DeleteDnsRecordCmd.class); + cmdList.add(RegisterDnsRecordForVmCmd.class); + cmdList.add(RemoveDnsRecordForVmCmd.class); return cmdList; } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java index 5b0309d16790..719b77430bfc 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java @@ -8,6 +8,6 @@ public interface DnsZoneNetworkMapDao extends GenericDao { List listByDnsZoneId(long dnsZoneId); - DnsZoneNetworkMapVO findByZoneAndNetwork(long zoneId, long networkId); + DnsZoneNetworkMapVO findByZoneAndNetwork(long dnsZoneId, long networkId); List listByNetworkId(long networkId); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java index dc21c953f974..209a77799488 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java @@ -2,6 +2,7 @@ import java.util.List; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; import org.springframework.stereotype.Component; @@ -11,28 +12,45 @@ @Component public class DnsZoneNetworkMapDaoImpl extends GenericDaoBase implements DnsZoneNetworkMapDao { - final SearchBuilder ZoneSearch; + private final SearchBuilder ZoneNetworkSearch; + private final SearchBuilder ZoneSearch; + private final SearchBuilder NetworkSearch; public DnsZoneNetworkMapDaoImpl() { super(); + ZoneNetworkSearch = createSearchBuilder(); + ZoneNetworkSearch.and(ApiConstants.DNS_ZONE_ID, ZoneNetworkSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ); + ZoneNetworkSearch.and(ApiConstants.NETWORK_ID, ZoneNetworkSearch.entity().getNetworkId(), SearchCriteria.Op.EQ); + ZoneNetworkSearch.done(); + ZoneSearch = createSearchBuilder(); - ZoneSearch.and("dnsZoneId", ZoneSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ); + ZoneSearch.and(ApiConstants.DNS_ZONE_ID, ZoneSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ); ZoneSearch.done(); + + NetworkSearch = createSearchBuilder(); + NetworkSearch.and(ApiConstants.NETWORK_ID, NetworkSearch.entity().getNetworkId(), SearchCriteria.Op.EQ); + NetworkSearch.done(); } @Override public List listByDnsZoneId(long dnsZoneId) { SearchCriteria sc = ZoneSearch.create(); - sc.setParameters("dnsZoneId", dnsZoneId); + sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId); return listBy(sc); } @Override - public DnsZoneNetworkMapVO findByZoneAndNetwork(long zoneId, long networkId) { - return null; + public DnsZoneNetworkMapVO findByZoneAndNetwork(long dnsZoneId, long networkId) { + SearchCriteria sc = ZoneNetworkSearch.create(); + sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId); + sc.setParameters(ApiConstants.NETWORK_ID, networkId); + + return findOneBy(sc); } @Override public List listByNetworkId(long networkId) { - return List.of(); + SearchCriteria sc = NetworkSearch.create(); + sc.setParameters(ApiConstants.NETWORK_ID, networkId); + return listBy(sc); } } From e011ce118668f36492e10e9db3767bee29acffac Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 18 Feb 2026 11:20:22 +0530 Subject: [PATCH 10/23] add missing license, cleanup, log std --- .../apache/cloudstack/dns/DnsProvider.java | 2 +- .../dns/powerdns/PowerDnsClient.java | 10 +++- .../dns/powerdns/PowerDnsProvider.java | 4 +- .../dns/DnsProviderManagerImpl.java | 57 ++++++++++--------- .../cloudstack/dns/dao/DnsServerDao.java | 4 -- .../cloudstack/dns/dao/DnsServerDaoImpl.java | 11 ---- .../apache/cloudstack/dns/dao/DnsZoneDao.java | 1 - .../cloudstack/dns/dao/DnsZoneDaoImpl.java | 11 ---- .../dns/dao/DnsZoneNetworkMapDao.java | 18 +++++- .../dns/dao/DnsZoneNetworkMapDaoImpl.java | 28 +++++---- .../apache/cloudstack/dns/vo/DnsZoneVO.java | 4 ++ 11 files changed, 77 insertions(+), 73 deletions(-) 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 f03f391f46c2..b9f4d733975c 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -28,7 +28,7 @@ public interface DnsProvider extends Adapter { void validate(DnsServer server) throws Exception; // Zone Operations - void provisionZone(DnsServer server, DnsZone zone); + String provisionZone(DnsServer server, DnsZone zone); void deleteZone(DnsServer server, DnsZone zone) ; void addRecord(DnsServer server, DnsZone zone, DnsRecord record); 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 a69e2e7f165e..b7c1a79e3777 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 @@ -95,7 +95,7 @@ public void validate(String baseUrl, String apiKey) { } } - public void createZone(String baseUrl, String apiKey, String zoneName, String nameServers) { + public String createZone(String baseUrl, String apiKey, String zoneName, String nameServers) { String normalizedZone = formatZoneName(zoneName); try { String url = buildApiUrl(baseUrl, "/servers/localhost/zones"); @@ -125,8 +125,12 @@ public void createZone(String baseUrl, String apiKey, String zoneName, String na String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; if (statusCode == HttpStatus.SC_CREATED) { - logger.debug("Zone {} created successfully", zoneName); - return; + JsonNode root = MAPPER.readTree(body); + String zoneId = root.path("id").asText(); + if (StringUtils.isBlank(zoneId)) { + throw new CloudRuntimeException("PowerDNS returned empty zone id"); + } + return zoneId; } if (statusCode == HttpStatus.SC_CONFLICT) { 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 cb4740fff9d8..8af0aa9806c9 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 @@ -46,9 +46,9 @@ public void validate(DnsServer server) { } @Override - public void provisionZone(DnsServer server, DnsZone zone) { + public String provisionZone(DnsServer server, DnsZone zone) { validateServerZoneParams(server, zone); - client.createZone(server.getUrl(), server.getApiKey(), zone.getName(), server.getNameServers()); + return client.createZone(server.getUrl(), server.getApiKey(), zone.getName(), server.getNameServers()); } @Override 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 740a5c31756b..7dedc3a8f16e 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.dns; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -96,7 +97,7 @@ private DnsProvider getProvider(DnsProviderType type) { return provider; } } - throw new CloudRuntimeException("No plugin found for DNS Provider type: " + type); + throw new CloudRuntimeException("No plugin found for DNS provider type: " + type); } @Override @@ -105,7 +106,7 @@ public DnsServer addDnsServer(AddDnsServerCmd cmd) { DnsServer existing = dnsServerDao.findByUrlAndAccount(cmd.getUrl(), caller.getId()); if (existing != null) { throw new InvalidParameterValueException( - "This Account already has a DNS Server integration for URL: " + cmd.getUrl()); + "This Account already has a DNS server integration for URL: " + cmd.getUrl()); } DnsProviderType type = DnsProviderType.fromString(cmd.getProvider()); DnsProvider provider = getProvider(type); @@ -156,7 +157,7 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { Long dnsServerId = cmd.getId(); DnsServerVO dnsServer = dnsServerDao.findById(dnsServerId); if (dnsServer == null) { - throw new InvalidParameterValueException(String.format("DNS Server with ID: %s not found.", dnsServerId)); + throw new InvalidParameterValueException(String.format("DNS server with ID: %s not found.", dnsServerId)); } Account caller = CallContext.current().getCallingAccount(); @@ -176,7 +177,7 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { if (!cmd.getUrl().equals(originalUrl)) { DnsServer duplicate = dnsServerDao.findByUrlAndAccount(cmd.getUrl(), dnsServer.getAccountId()); if (duplicate != null && duplicate.getId() != dnsServer.getId()) { - throw new InvalidParameterValueException("Another DNS Server with this URL already exists."); + throw new InvalidParameterValueException("Another DNS server with this URL already exists."); } dnsServer.setUrl(cmd.getUrl()); validationRequired = true; @@ -230,7 +231,7 @@ public boolean deleteDnsServer(DeleteDnsServerCmd cmd) { Long dnsServerId = cmd.getId(); DnsServerVO dnsServer = dnsServerDao.findById(dnsServerId); if (dnsServer == null) { - throw new InvalidParameterValueException(String.format("DNS Server with ID: %s not found.", dnsServerId)); + throw new InvalidParameterValueException(String.format("DNS server with ID: %s not found.", dnsServerId)); } Account caller = CallContext.current().getCallingAccount(); if (!accountMgr.isRootAdmin(caller.getId()) && dnsServer.getAccountId() != caller.getId()) { @@ -260,7 +261,7 @@ public DnsServer getDnsServer(Long id) { public boolean deleteDnsZone(Long zoneId) { DnsZoneVO zone = dnsZoneDao.findById(zoneId); if (zone == null) { - throw new InvalidParameterValueException("DNS Zone with ID " + zoneId + " not found."); + throw new InvalidParameterValueException("DNS zone with ID " + zoneId + " not found."); } Account caller = CallContext.current().getCallingAccount(); @@ -269,10 +270,10 @@ public boolean deleteDnsZone(Long zoneId) { if (server != null && zone.getState() == DnsZone.State.Active) { try { DnsProvider provider = getProvider(server.getProviderType()); - logger.debug("Deleting DNS zone {} from provider.", zone.getName()); + logger.debug("Deleting DNS zone: {} from provider.", zone.getName()); provider.deleteZone(server, zone); } catch (Exception ex) { - logger.error("Failed to delete zone from provider", ex); + logger.error("Failed to delete DNS zone from provider", ex); throw new CloudRuntimeException("Failed to delete DNS zone."); } } @@ -336,7 +337,7 @@ public DnsZone getDnsZone(long id) { public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); if (zone == null) { - throw new InvalidParameterValueException("DNS Zone not found."); + throw new InvalidParameterValueException("DNS zone not found."); } Account caller = CallContext.current().getCallingAccount(); @@ -358,7 +359,7 @@ public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) { DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); if (zone == null) { - throw new InvalidParameterValueException("DNS Zone not found."); + throw new InvalidParameterValueException("DNS zone not found."); } Account caller = CallContext.current().getCallingAccount(); @@ -376,7 +377,7 @@ public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) { return true; } catch (Exception ex) { logger.error("Failed to delete DNS record via provider", ex); - throw new CloudRuntimeException(String.format("Failed to delete record: %s", cmd.getName())); + throw new CloudRuntimeException(String.format("Failed to delete DNS record: %s", cmd.getName())); } } @@ -384,13 +385,13 @@ public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) { public ListResponse listDnsRecords(ListDnsRecordsCmd cmd) { DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); if (zone == null) { - throw new InvalidParameterValueException(String.format("DNS Zone with ID %s not found.", cmd.getDnsZoneId())); + throw new InvalidParameterValueException(String.format("DNS zone with ID %s not found.", cmd.getDnsZoneId())); } Account caller = CallContext.current().getCallingAccount(); accountMgr.checkAccess(caller, null, true, zone); DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); if (server == null) { - throw new CloudRuntimeException("The underlying DNS Server for this zone is missing."); + throw new CloudRuntimeException("The underlying DNS server for this DNS zone is missing."); } try { DnsProvider provider = getProvider(server.getProviderType()); @@ -425,23 +426,23 @@ public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) { Account caller = CallContext.current().getCallingAccount(); DnsServerVO server = dnsServerDao.findById(cmd.getDnsServerId()); if (server == null) { - throw new InvalidParameterValueException("DNS Server not found"); + throw new InvalidParameterValueException("DNS server not found"); } boolean isOwner = (server.getAccountId() == caller.getId()); if (!server.isPublic() && !isOwner) { - throw new PermissionDeniedException("You do not have permission to use this DNS Server."); + throw new PermissionDeniedException("You do not have permission to use this DNS server."); } DnsZone.ZoneType type = DnsZone.ZoneType.Public; if (cmd.getType() != null) { try { type = DnsZone.ZoneType.valueOf(cmd.getType()); } catch (IllegalArgumentException e) { - throw new InvalidParameterValueException("Invalid Zone Type"); + throw new InvalidParameterValueException("Invalid DNS zone Type"); } } DnsZoneVO existing = dnsZoneDao.findByNameServerAndType(cmd.getName(), server.getId(), type); if (existing != null) { - throw new InvalidParameterValueException("Zone already exists on this server."); + throw new InvalidParameterValueException("DNS zone already exists on this server."); } DnsZoneVO dnsZoneVO = new DnsZoneVO(cmd.getName(), type, server.getId(), caller.getId(), caller.getDomainId(), cmd.getDescription()); return dnsZoneDao.persist(dnsZoneVO); @@ -451,20 +452,20 @@ public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) { public DnsZone provisionDnsZone(long zoneId) { DnsZoneVO dnsZone = dnsZoneDao.findById(zoneId); if (dnsZone == null) { - throw new CloudRuntimeException("DNS Zone not found during provisioning"); + throw new CloudRuntimeException("DNS zone not found during provisioning"); } DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId()); - try { DnsProvider provider = getProvider(server.getProviderType()); - logger.debug("Provision DNS zone: {} on DNS server: {}", dnsZone.getName(), server.getName()); - provider.provisionZone(server, dnsZone); + String externalReferenceId = provider.provisionZone(server, dnsZone); + dnsZone.setExternalReference(externalReferenceId); dnsZone.setState(DnsZone.State.Active); + logger.debug("DNS zone: {} created successfully on DNS server: {} with ID: {}", dnsZone.getName(), server.getName(), zoneId); dnsZoneDao.update(dnsZone.getId(), dnsZone); } catch (Exception ex) { - logger.error("Failed to provision zone: {} on server: {}", dnsZone.getName(), server.getName(), ex); + logger.error("Failed to provision DNS zone: {} on DNS server: {}", dnsZone.getName(), server.getName(), ex); dnsZoneDao.remove(zoneId); - throw new CloudRuntimeException("Failed to provision zone: " + dnsZone.getName()); + throw new CloudRuntimeException("Failed to provision DNS zone: " + dnsZone.getName()); } return dnsZone; } @@ -495,7 +496,7 @@ public DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetwor Account caller = CallContext.current().getCallingAccount(); DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); if (zone == null) { - throw new InvalidParameterValueException("DNS Zone not found."); + throw new InvalidParameterValueException("DNS zone not found."); } accountMgr.checkAccess(caller, null, true, zone); @@ -510,7 +511,7 @@ public DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetwor accountMgr.checkAccess(caller, null, true, network); DnsZoneNetworkMapVO existing = dnsZoneNetworkMapDao.findByZoneAndNetwork(zone.getId(), network.getId()); if (existing != null) { - throw new InvalidParameterValueException("This DNS Zone is already associated with this Network."); + throw new InvalidParameterValueException("This DNS zone is already associated with this Network."); } DnsZoneNetworkMapVO mapping = new DnsZoneNetworkMapVO(zone.getId(), network.getId(), cmd.getSubDomain()); dnsZoneNetworkMapDao.persist(mapping); @@ -556,7 +557,7 @@ private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boo // 1. Fetch VM and verify access UserVmVO instance = userVmDao.findById(instanceId); if (instance == null) { - throw new InvalidParameterValueException("Instance not found."); + throw new InvalidParameterValueException("Provided Instance not found."); } accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, instance); @@ -599,7 +600,7 @@ private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boo DnsProvider provider = getProvider(server.getProviderType()); // Handle IPv4 (A Record) if (nic.getIPv4Address() != null) { - DnsRecord recordA = new DnsRecord(recordName, DnsRecord.RecordType.A, java.util.Arrays.asList(nic.getIPv4Address()), 3600); + DnsRecord recordA = new DnsRecord(recordName, DnsRecord.RecordType.A, Collections.singletonList(nic.getIPv4Address()), 3600); if (isAdd) { provider.addRecord(server, zone, recordA); } else { @@ -610,7 +611,7 @@ private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boo // Handle IPv6 (AAAA Record) if it exists if (nic.getIPv6Address() != null) { - DnsRecord recordAAAA = new DnsRecord(recordName, DnsRecord.RecordType.AAAA, java.util.Arrays.asList(nic.getIPv6Address()), 3600); + DnsRecord recordAAAA = new DnsRecord(recordName, DnsRecord.RecordType.AAAA, Collections.singletonList(nic.getIPv6Address()), 3600); if (isAdd) { provider.addRecord(server, zone, recordAAAA); } else { diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java index 0c64f742b634..5cbf5e3256d9 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java @@ -27,10 +27,6 @@ import com.cloud.utils.db.GenericDao; public interface DnsServerDao extends GenericDao { - - List listByProvider(String provider); - DnsServer findByUrlAndAccount(String url, long accountId); - Pair, Integer> searchDnsServers(Long id, String keyword, String provider, Long accountId, Filter filter); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java index e9deb9bb2ca2..7e4559c51fd2 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java @@ -33,15 +33,11 @@ @Component public class DnsServerDaoImpl extends GenericDaoBase implements DnsServerDao { SearchBuilder AllFieldsSearch; - SearchBuilder ProviderSearch; SearchBuilder AccountUrlSearch; public DnsServerDaoImpl() { super(); - ProviderSearch = createSearchBuilder(); - ProviderSearch.and(ApiConstants.PROVIDER_TYPE, ProviderSearch.entity().getProviderType(), SearchCriteria.Op.EQ); - ProviderSearch.done(); AccountUrlSearch = createSearchBuilder(); AccountUrlSearch.and(ApiConstants.URL, AccountUrlSearch.entity().getUrl(), SearchCriteria.Op.EQ); @@ -57,13 +53,6 @@ public DnsServerDaoImpl() { } - @Override - public List listByProvider(String providerType) { - SearchCriteria sc = ProviderSearch.create(); - sc.setParameters(ApiConstants.PROVIDER_TYPE, providerType); - return listBy(sc); - } - @Override public DnsServer findByUrlAndAccount(String url, long accountId) { SearchCriteria sc = AccountUrlSearch.create(); diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java index a2489323e8e3..1d58b5275036 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java @@ -27,7 +27,6 @@ import com.cloud.utils.db.GenericDao; public interface DnsZoneDao extends GenericDao { - List listByServerId(long serverId); List listByAccount(long accountId); DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type); Pair, Integer> searchZones(Long id, Long dnsServerId, String keyword, Long accountId, Filter filter); diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java index b45d10723a19..2658b8e2987a 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java @@ -33,16 +33,12 @@ @Component public class DnsZoneDaoImpl extends GenericDaoBase implements DnsZoneDao { static final String DNS_SERVER_ID = "dnsServerId"; - SearchBuilder ServerSearch; SearchBuilder AccountSearch; SearchBuilder NameServerTypeSearch; SearchBuilder AllFieldsSearch; public DnsZoneDaoImpl() { super(); - ServerSearch = createSearchBuilder(); - ServerSearch.and(DNS_SERVER_ID, ServerSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); - ServerSearch.done(); AccountSearch = createSearchBuilder(); AccountSearch.and(ApiConstants.ACCOUNT_ID, AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); @@ -62,13 +58,6 @@ public DnsZoneDaoImpl() { AllFieldsSearch.done(); } - @Override - public List listByServerId(long serverId) { - SearchCriteria sc = ServerSearch.create(); - sc.setParameters(DNS_SERVER_ID, serverId); - return listBy(sc); - } - @Override public List listByAccount(long accountId) { SearchCriteria sc = AccountSearch.create(); diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java index 719b77430bfc..1d522d0f16b6 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java @@ -1,3 +1,20 @@ +// 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; @@ -7,7 +24,6 @@ import com.cloud.utils.db.GenericDao; public interface DnsZoneNetworkMapDao extends GenericDao { - List listByDnsZoneId(long dnsZoneId); DnsZoneNetworkMapVO findByZoneAndNetwork(long dnsZoneId, long networkId); List listByNetworkId(long networkId); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java index 209a77799488..aec93a500ad5 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java @@ -1,3 +1,20 @@ +// 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; @@ -13,7 +30,6 @@ @Component public class DnsZoneNetworkMapDaoImpl extends GenericDaoBase implements DnsZoneNetworkMapDao { private final SearchBuilder ZoneNetworkSearch; - private final SearchBuilder ZoneSearch; private final SearchBuilder NetworkSearch; public DnsZoneNetworkMapDaoImpl() { @@ -23,20 +39,10 @@ public DnsZoneNetworkMapDaoImpl() { ZoneNetworkSearch.and(ApiConstants.NETWORK_ID, ZoneNetworkSearch.entity().getNetworkId(), SearchCriteria.Op.EQ); ZoneNetworkSearch.done(); - ZoneSearch = createSearchBuilder(); - ZoneSearch.and(ApiConstants.DNS_ZONE_ID, ZoneSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ); - ZoneSearch.done(); - NetworkSearch = createSearchBuilder(); NetworkSearch.and(ApiConstants.NETWORK_ID, NetworkSearch.entity().getNetworkId(), SearchCriteria.Op.EQ); NetworkSearch.done(); } - @Override - public List listByDnsZoneId(long dnsZoneId) { - SearchCriteria sc = ZoneSearch.create(); - sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId); - return listBy(sc); - } @Override public DnsZoneNetworkMapVO findByZoneAndNetwork(long dnsZoneId, long networkId) { diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java index 2988ee49acd4..ca0817208b92 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java @@ -157,4 +157,8 @@ public long getDomainId() { public void setDescription(String description) { this.description = description; } + + public void setExternalReference(String externalReference) { + this.externalReference = externalReference; + } } From 4a9f66d532e21f4ae32bb01d2122d171c9d2218b Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Thu, 19 Feb 2026 23:48:57 +0530 Subject: [PATCH 11/23] following changes are done: 1. refactored client 2. added exceptions 3. enhanced updateZone 4. ownership check for deleteDnsServer --- .../apache/cloudstack/api/ApiConstants.java | 13 + .../command/user/dns/CreateDnsZoneCmd.java | 2 +- .../apache/cloudstack/dns/DnsProvider.java | 15 +- .../cloudstack/dns/DnsProviderManager.java | 1 - .../org/apache/cloudstack/dns/DnsServer.java | 6 +- .../exception/DnsAuthenticationException.java | 27 + .../dns/exception/DnsConflictException.java | 27 + .../dns/exception/DnsNotFoundException.java | 27 + .../dns/exception/DnsOperationException.java | 27 + .../dns/exception/DnsProviderException.java | 28 + .../dns/exception/DnsTransportException.java | 30 ++ .../META-INF/db/schema-42210to42300.sql | 1 + .../dns/powerdns/PowerDnsClient.java | 477 +++++++----------- .../dns/powerdns/PowerDnsProvider.java | 46 +- .../dns/powerdns/PowerDnsClientTest.java | 84 +++ .../dns/DnsProviderManagerImpl.java | 64 +-- .../apache/cloudstack/dns/vo/DnsServerVO.java | 24 +- 17 files changed, 549 insertions(+), 350 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java create mode 100644 api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java create mode 100644 api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java create mode 100644 api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java create mode 100644 api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java create mode 100644 api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java create mode 100644 plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java 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 5dbb200d32ad..605ff28d50e0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1337,8 +1337,21 @@ public class ApiConstants { public static final String NAME_SERVERS = "nameservers"; public static final String CREDENTIALS = "credentials"; public static final String DNS_ZONE_ID = "dnszoneid"; + public static final String DNS_SERVER_ID = "dnsserverid"; + public static final String CONTENT = "content"; public static final String CONTENTS = "contents"; public static final String PUBLIC_DOMAIN_SUFFIX = "publicdomainsuffix"; + public static final String AUTHORITATIVE = "authoritative"; + public static final String KIND = "kind"; + public static final String DNS_SEC = "dnssec"; + public static final String TTL = "ttl"; + public static final String CHANGE_TYPE = "changetype"; + public static final String RECORDS = "records"; + public static final String RR_SETS = "rrsets"; + public static final String X_API_KEY = "X-API-Key"; + public static final String DISABLED = "disabled"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String PARAMETER_DESCRIPTION_ACTIVATION_RULE = "Quota tariff's activation rule. It can receive a JS script that results in either " + "a boolean or a numeric value: if it results in a boolean value, the tariff value will be applied according to the result; if it results in a numeric value, the " + diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java index 467b0c787969..ab0ac9cf4c36 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java @@ -51,7 +51,7 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { description = "The name of the DNS zone (e.g. example.com)") private String name; - @Parameter(name = "dnsserverid", type = CommandType.UUID, entityType = DnsServerResponse.class, + @Parameter(name = ApiConstants.DNS_SERVER_ID, type = CommandType.UUID, entityType = DnsServerResponse.class, required = true, description = "The ID of the DNS server to host this zone") private Long dnsServerId; 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 b9f4d733975c..7d4ab1133b72 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -19,6 +19,8 @@ import java.util.List; +import org.apache.cloudstack.dns.exception.DnsProviderException; + import com.cloud.utils.component.Adapter; public interface DnsProvider extends Adapter { @@ -28,11 +30,12 @@ public interface DnsProvider extends Adapter { void validate(DnsServer server) throws Exception; // Zone Operations - String provisionZone(DnsServer server, DnsZone zone); - void deleteZone(DnsServer server, DnsZone zone) ; + String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException; + void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException; + void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException; - void addRecord(DnsServer server, DnsZone zone, DnsRecord record); - List listRecords(DnsServer server, DnsZone zone); - void updateRecord(DnsServer server, DnsZone zone, DnsRecord record); - void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record); + String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; + List listRecords(DnsServer server, DnsZone zone) throws DnsProviderException; + String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; + void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; } 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 e3b7431803fb..2adb1d8f3c5b 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -61,7 +61,6 @@ public interface DnsProviderManager extends Manager, PluggableService { DnsZone updateDnsZone(UpdateDnsZoneCmd cmd); boolean deleteDnsZone(Long id); ListResponse listDnsZones(ListDnsZonesCmd cmd); - DnsZone getDnsZone(long id); DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd); boolean deleteDnsRecord(DeleteDnsRecordCmd cmd); diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java index 617352cbfe38..14160cdf1ffc 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java @@ -18,11 +18,13 @@ package org.apache.cloudstack.dns; import java.util.Date; +import java.util.List; +import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; -public interface DnsServer extends InternalIdentity, Identity { +public interface DnsServer extends InternalIdentity, Identity, ControlledEntity { enum State { Enabled, Disabled }; @@ -33,7 +35,7 @@ enum State { DnsProviderType getProviderType(); - String getNameServers(); + List getNameServers(); String getApiKey(); diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java new file mode 100644 index 000000000000..325cb78241ef --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java @@ -0,0 +1,27 @@ +// 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.exception; + +/** + * Thrown when authentication to the DNS provider fails. + */ +public class DnsAuthenticationException extends DnsProviderException { + public DnsAuthenticationException(String message) { + super(message); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java new file mode 100644 index 000000000000..9a36bb87478a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java @@ -0,0 +1,27 @@ +// 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.exception; + +/** + * Thrown when attempting to create a zone or record that already exists. + */ +public class DnsConflictException extends DnsProviderException { + public DnsConflictException(String message) { + super(message); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java new file mode 100644 index 000000000000..aa88f308ce84 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java @@ -0,0 +1,27 @@ +// 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.exception; + +/** + * Thrown when the requested zone or record does not exist. + */ +public class DnsNotFoundException extends DnsProviderException { + public DnsNotFoundException(String message) { + super(message); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java new file mode 100644 index 000000000000..564acdc9a6f0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java @@ -0,0 +1,27 @@ +// 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.exception; + +/** + * Thrown for unexpected or unknown errors returned by the DNS provider. + */ +public class DnsOperationException extends DnsProviderException { + public DnsOperationException(String message) { + super(message); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java new file mode 100644 index 000000000000..de307c9903e4 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java @@ -0,0 +1,28 @@ +// 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.exception; + +public class DnsProviderException extends Exception { + public DnsProviderException(String message) { + super(message); + } + + public DnsProviderException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java new file mode 100644 index 000000000000..50f04143c921 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java @@ -0,0 +1,30 @@ +// 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.exception; + +import java.io.IOException; + +/** + * Thrown when HTTP or network errors occur communicating with the DNS provider. + */ +public class DnsTransportException extends DnsProviderException { + + public DnsTransportException(String message, IOException cause) { + super(message, cause); + } +} 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 06054c7cce8e..208991bf4b38 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 @@ -68,6 +68,7 @@ CREATE TABLE `cloud`.`dns_server` ( `is_public` tinyint(1) NOT NULL DEFAULT '0', `public_domain_suffix` VARCHAR(255), `state` ENUM('Enabled', 'Disabled') NOT NULL DEFAULT 'Disabled', + `domain_id` bigint unsigned COMMENT 'for domain-specific ownership', `account_id` bigint(20) unsigned NOT NULL, `created` datetime NOT NULL COMMENT 'date created', `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', 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 b7c1a79e3777..28be4f5155ec 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 @@ -20,28 +20,34 @@ import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; - +import java.util.concurrent.TimeUnit; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.exception.DnsAuthenticationException; +import org.apache.cloudstack.dns.exception.DnsConflictException; +import org.apache.cloudstack.dns.exception.DnsNotFoundException; +import org.apache.cloudstack.dns.exception.DnsOperationException; +import org.apache.cloudstack.dns.exception.DnsProviderException; +import org.apache.cloudstack.dns.exception.DnsTransportException; import org.apache.commons.collections.CollectionUtils; -import org.apache.http.HttpStatus; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.cloud.utils.StringUtils; -import com.cloud.utils.exception.CloudRuntimeException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -50,359 +56,250 @@ public class PowerDnsClient implements AutoCloseable { public static final Logger logger = LoggerFactory.getLogger(PowerDnsClient.class); private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final int TIMEOUT_MS = 5000; - private final CloseableHttpClient httpClient; - - public void validate(String baseUrl, String apiKey) { - String checkUrl = buildApiUrl(baseUrl, "/servers"); - HttpGet request = new HttpGet(checkUrl); - request.addHeader("X-API-Key", apiKey); - request.addHeader("Content-Type", "application/json"); - request.addHeader("Accept", "application/json"); - try (CloseableHttpResponse response = httpClient.execute(request)) { - int statusCode = response.getStatusLine().getStatusCode(); - String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; + private static final int CONNECT_TIMEOUT_MS = 5_000; + private static final int SOCKET_TIMEOUT_MS = 10_000; + private static final int MAX_CONNECTIONS_TOTAL = 50; + private static final int MAX_CONNECTIONS_PER_ROUTE = 10; + private static final String API_PREFIX = "/api/v1"; + private static final String DEFAULT_SERVER = "localhost"; - if (statusCode == HttpStatus.SC_OK) { - JsonNode root = MAPPER.readTree(body); - - if (!root.isArray() || root.isEmpty()) { - throw new CloudRuntimeException("No servers returned by PowerDNS API"); - } + private final CloseableHttpClient httpClient; - boolean authoritativeFound = false; - for (JsonNode node : root) { - if ("authoritative".equalsIgnoreCase(node.path("daemon_type").asText(null))) { - authoritativeFound = true; - break; - } - } + public PowerDnsClient() { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(MAX_CONNECTIONS_TOTAL); + connectionManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE); + + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(CONNECT_TIMEOUT_MS) + .setConnectionRequestTimeout(CONNECT_TIMEOUT_MS) + .setSocketTimeout(SOCKET_TIMEOUT_MS) + .build(); - if (!authoritativeFound) { - throw new CloudRuntimeException("No authoritative PowerDNS server found"); - } + this.httpClient = HttpClientBuilder.create() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .evictIdleConnections(30, TimeUnit.SECONDS) + .disableCookieManagement() + .build(); + } - } else if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { - throw new CloudRuntimeException("Invalid PowerDNS API key"); - } else { - logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body); - throw new CloudRuntimeException(String.format("PowerDNS validation failed with HTTP %d", statusCode)); + public void validate(String baseUrl, String apiKey) throws DnsProviderException { + String url = buildUrl(baseUrl, "/servers"); + HttpGet request = new HttpGet(url); + JsonNode servers = execute(request, apiKey, 200); + if (servers == null || !servers.isArray() || servers.isEmpty()) { + throw new DnsOperationException("No servers returned by PowerDNS API"); + } + boolean authoritativeFound = false; + for (JsonNode server : servers) { + if (ApiConstants.AUTHORITATIVE.equalsIgnoreCase(server.path("daemon_type").asText(null))) { + authoritativeFound = true; + break; } - - } catch (IOException ex) { - throw new CloudRuntimeException("Failed to connect to PowerDNS", ex); + } + if (!authoritativeFound) { + throw new DnsOperationException("No authoritative PowerDNS server found"); } } - public String createZone(String baseUrl, String apiKey, String zoneName, String nameServers) { - String normalizedZone = formatZoneName(zoneName); - try { - String url = buildApiUrl(baseUrl, "/servers/localhost/zones"); - ObjectNode json = MAPPER.createObjectNode(); - json.put("name", normalizedZone); - json.put("kind", "Native"); - json.put("dnssec", false); + public String createZone(String baseUrl, String apiKey, String zoneName, String zoneKind, boolean dnsSecFlag, + List nameServers) throws DnsProviderException { - if (StringUtils.isNotEmpty(nameServers)) { - List nsNames = new ArrayList<>(Arrays.asList(nameServers.split(","))); - if (!CollectionUtils.isEmpty(nsNames)) { - ArrayNode nsArray = json.putArray("nameservers"); - for (String ns : nsNames) { - nsArray.add(ns.endsWith(".") ? ns : ns + "."); - } - } - } - HttpPost request = new HttpPost(url); - request.addHeader("X-API-Key", apiKey); - request.addHeader("Content-Type", "application/json"); - request.addHeader("Accept", "application/json"); - request.setEntity(new StringEntity(json.toString())); - - try (CloseableHttpResponse response = httpClient.execute(request)) { + validate(baseUrl, apiKey); - int statusCode = response.getStatusLine().getStatusCode(); - String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; - - if (statusCode == HttpStatus.SC_CREATED) { - JsonNode root = MAPPER.readTree(body); - String zoneId = root.path("id").asText(); - if (StringUtils.isBlank(zoneId)) { - throw new CloudRuntimeException("PowerDNS returned empty zone id"); - } - return zoneId; - } - - if (statusCode == HttpStatus.SC_CONFLICT) { - throw new CloudRuntimeException("Zone already exists: " + zoneName); - } - - if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { - throw new CloudRuntimeException("Invalid PowerDNS API key"); - } - - logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body); - throw new CloudRuntimeException(String.format("Failed to create zone %s (HTTP %d)", zoneName, statusCode)); + String normalizedZone = normalizeZone(zoneName); + ObjectNode json = MAPPER.createObjectNode(); + json.put(ApiConstants.NAME, normalizedZone); + json.put(ApiConstants.KIND, zoneKind); + json.put(ApiConstants.DNS_SEC, dnsSecFlag); + if (!CollectionUtils.isEmpty(nameServers)) { + ArrayNode nsArray = json.putArray(ApiConstants.NAME_SERVERS); + for (String ns : nameServers) { + nsArray.add(ns.endsWith(".") ? ns : ns + "."); } - } catch (IOException e) { - throw new CloudRuntimeException("Error while creating PowerDNS zone " + zoneName, e); } + HttpPost request = new HttpPost(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones")); + request.setEntity(new StringEntity(json.toString(), StandardCharsets.UTF_8)); + JsonNode response = execute(request, apiKey, 201); + if (response == null) { + throw new DnsOperationException("Empty response from DNS server"); + } + String zoneId = response.path(ApiConstants.ID).asText(); + if (StringUtils.isBlank(zoneId)) { + throw new DnsOperationException("PowerDNS returned empty zone id"); + } + return zoneId; } - public void deleteZone(String baseUrl, String apiKey, String zoneName) { - String normalizedZone = formatZoneName(zoneName); - try { - String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); - String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone); - HttpDelete request = new HttpDelete(url); - request.addHeader("X-API-Key", apiKey); - request.addHeader("Content-Type", "application/json"); - request.addHeader("Accept", "application/json"); - - try (CloseableHttpResponse response = httpClient.execute(request)) { + public void updateZone(String baseUrl, String apiKey, String zoneName, String zoneKind, Boolean dnsSecFlag, + List nameServers) throws DnsProviderException { - int statusCode = response.getStatusLine().getStatusCode(); - String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; - - if (statusCode == HttpStatus.SC_NO_CONTENT) { - logger.debug("Zone {} deleted successfully", normalizedZone); - return; - } + String normalizedZone = normalizeZone(zoneName); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + String url = buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone); - if (statusCode == HttpStatus.SC_NOT_FOUND) { - logger.debug("Zone {} not found in PowerDNS", normalizedZone); - return; - } - - if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { - throw new CloudRuntimeException("Invalid PowerDNS API key"); - } + ObjectNode json = MAPPER.createObjectNode(); - logger.debug("Unexpected PowerDNS response while deleting zone: HTTP {} Body: {}", statusCode, body); - throw new CloudRuntimeException(String.format("Failed to delete zone %s (HTTP %d)", normalizedZone, statusCode)); + if (dnsSecFlag != null) { + json.put(ApiConstants.DNS_SEC, dnsSecFlag); + } + if (StringUtils.isNotBlank(zoneKind)) { + json.put(ApiConstants.KIND, zoneKind); + } + if (!CollectionUtils.isEmpty(nameServers)) { + ArrayNode nsArray = json.putArray(ApiConstants.NAME_SERVERS); + for (String ns : nameServers) { + nsArray.add(ns.endsWith(".") ? ns : ns + "."); } - } catch (IOException e) { - throw new CloudRuntimeException("Error while deleting PowerDNS zone " + zoneName, e); } + HttpPatch request = new HttpPatch(url); + request.setEntity(new org.apache.http.entity.StringEntity(json.toString(), StandardCharsets.UTF_8)); + execute(request, apiKey, 204); } - public void modifyRecord(String baseUrl, String apiKey, String zoneName, String recordName, String type, long ttl, List contents, String changeType) { - String normalizedZone = formatZoneName(zoneName); - String normalizedRecord = formatRecordName(recordName, zoneName); - - try { - String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); - String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone); - - ObjectNode root = MAPPER.createObjectNode(); - ArrayNode rrsets = root.putArray("rrsets"); - ObjectNode rrset = rrsets.addObject(); - - rrset.put("name", normalizedRecord); - rrset.put("type", type.toUpperCase()); - rrset.put("ttl", ttl); - rrset.put("changetype", changeType); - - ArrayNode records = rrset.putArray("records"); - if (!CollectionUtils.isEmpty(contents)) { - for (String content : contents) { - ObjectNode record = records.addObject(); - record.put("content", content); - record.put("disabled", false); - } - } - - HttpPatch request = new HttpPatch(url); - request.addHeader("X-API-Key", apiKey); - request.addHeader("Content-Type", "application/json"); - request.addHeader("Accept", "application/json"); - request.setEntity(new StringEntity(root.toString())); - - try (CloseableHttpResponse response = httpClient.execute(request)) { - int statusCode = response.getStatusLine().getStatusCode(); - String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; - - if (statusCode == HttpStatus.SC_NO_CONTENT) { - logger.debug("Record {} {} added/updated in zone {}", normalizedRecord, type, normalizedZone); - return; - } - - if (statusCode == HttpStatus.SC_NOT_FOUND) { - throw new CloudRuntimeException("Zone not found: " + normalizedZone); - } - - if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { - throw new CloudRuntimeException("Invalid PowerDNS API key"); - } + public void deleteZone(String baseUrl, String apiKey, String zoneName) throws DnsProviderException { + validate(baseUrl, apiKey); + String normalizedZone = normalizeZone(zoneName); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + HttpDelete request = new HttpDelete(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone)); + execute(request, apiKey, 204, 404); + } - logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body); - throw new CloudRuntimeException("Failed to add/update record " + normalizedRecord); + public String modifyRecord(String baseUrl, String apiKey, String zoneName, String recordName, String type, long ttl, + List contents, String changeType) throws DnsProviderException { + + validate(baseUrl, apiKey); + String normalizedZone = normalizeZone(zoneName); + String normalizedRecord = normalizeRecordName(recordName, normalizedZone); + ObjectNode root = MAPPER.createObjectNode(); + ArrayNode rrsets = root.putArray(ApiConstants.RR_SETS); + ObjectNode rrset = rrsets.addObject(); + rrset.put(ApiConstants.NAME, normalizedRecord); + rrset.put(ApiConstants.TYPE, type.toUpperCase()); + rrset.put(ApiConstants.TTL, ttl); + rrset.put(ApiConstants.CHANGE_TYPE, changeType); + ArrayNode records = rrset.putArray(ApiConstants.RECORDS); + if (!CollectionUtils.isEmpty(contents)) { + for (String content : contents) { + ObjectNode record = records.addObject(); + record.put(ApiConstants.CONTENT, content); + record.put(ApiConstants.DISABLED, false); } - - } catch (IOException e) { - throw new CloudRuntimeException("Error while adding PowerDNS record", e); } + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + HttpPatch request = new HttpPatch(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone)); + request.setEntity(new org.apache.http.entity.StringEntity(root.toString(), StandardCharsets.UTF_8)); + execute(request, apiKey, 204); + return normalizedRecord; } - public void deleteRecord(String baseUrl, String apiKey, String zoneName, String recordName, String type) { - - String normalizedZone = formatZoneName(zoneName); - String normalizedRecord = formatRecordName(recordName, zoneName); - - try { - String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); - String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone); - - ObjectNode root = MAPPER.createObjectNode(); - ArrayNode rrsets = root.putArray("rrsets"); - ObjectNode rrset = rrsets.addObject(); - - rrset.put("name", normalizedRecord); - rrset.put("type", type.toUpperCase()); - rrset.put("changetype", "DELETE"); - - HttpPatch request = new HttpPatch(url); - request.addHeader("X-API-Key", apiKey); - request.addHeader("Content-Type", "application/json"); - request.addHeader("Accept", "application/json"); - request.setEntity(new StringEntity(root.toString())); - - try (CloseableHttpResponse response = httpClient.execute(request)) { - int statusCode = response.getStatusLine().getStatusCode(); - String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; - - if (statusCode == HttpStatus.SC_NO_CONTENT) { - logger.debug("Record {} {} deleted", normalizedRecord, type); - return; - } - - if (statusCode == HttpStatus.SC_NOT_FOUND) { - logger.debug("Record {} {} not found (idempotent delete)", normalizedRecord, type); - return; - } - - if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { - throw new CloudRuntimeException("Invalid PowerDNS API key"); - } - - logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body); - throw new CloudRuntimeException("Failed to delete record " + normalizedRecord); - } - - } catch (IOException e) { - throw new CloudRuntimeException("Error while deleting PowerDNS record", e); + public Iterable listRecords(String baseUrl, String apiKey, String zoneName) throws DnsProviderException { + validate(baseUrl, apiKey); + String normalizedZone = normalizeZone(zoneName); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + HttpGet request = new HttpGet(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone)); + JsonNode zoneNode = execute(request, apiKey, 200); + if (zoneNode == null || !zoneNode.has(ApiConstants.RR_SETS)) { + return Collections.emptyList(); } + JsonNode rrsets = zoneNode.path(ApiConstants.RR_SETS); + return rrsets.isArray() ? rrsets : Collections.emptyList(); } - public Iterable listRecords(String baseUrl, String apiKey, String zoneName) { - String normalizedZone = formatZoneName(zoneName); - try { - String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); - String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone); - - HttpGet request = new HttpGet(url); - request.addHeader("X-API-Key", apiKey); - request.addHeader("Accept", "application/json"); - - try (CloseableHttpResponse response = httpClient.execute(request)) { - - int statusCode = response.getStatusLine().getStatusCode(); - String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; + private JsonNode execute(HttpUriRequest request, String apiKey, int... expectedStatus) throws DnsProviderException { + request.addHeader(ApiConstants.X_API_KEY, apiKey); + request.addHeader("Accept", "application/json"); + request.addHeader(ApiConstants.CONTENT_TYPE, "application/json"); - if (statusCode == HttpStatus.SC_OK) { - JsonNode zone = MAPPER.readTree(body); - JsonNode rrsets = zone.path("rrsets"); + try (CloseableHttpResponse response = httpClient.execute(request)) { + int status = response.getStatusLine().getStatusCode(); + String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null; - if (rrsets.isArray()) { - return rrsets; + for (int expected : expectedStatus) { + if (status == expected) { + if (body != null && !body.isEmpty()) { + return MAPPER.readTree(body); + } else { + return null; } - - return Collections.emptyList(); - } - - if (statusCode == HttpStatus.SC_NOT_FOUND) { - throw new CloudRuntimeException("Zone not found: " + normalizedZone); - } - - if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { - throw new CloudRuntimeException("Invalid PowerDNS API key"); } - - throw new CloudRuntimeException("Failed to list records for zone " + normalizedZone); } - - } catch (IOException e) { - throw new CloudRuntimeException("Error while listing PowerDNS records", e); + if (status == 404) { + throw new DnsNotFoundException("Resource not found: " + body); + } else if (status == 401 || status == 403) { + throw new DnsAuthenticationException("Invalid API key"); + } else if (status == 409) { + throw new DnsConflictException("Conflict: " + body); + } + throw new DnsOperationException("Unexpected PowerDNS response: HTTP " + status + " Body: " + body); + } catch (IOException ex) { + throw new DnsTransportException("Error communicating with PowerDNS", ex); } } - public PowerDnsClient() { - RequestConfig config = RequestConfig.custom() - .setConnectTimeout(TIMEOUT_MS) - .setConnectionRequestTimeout(TIMEOUT_MS) - .setSocketTimeout(TIMEOUT_MS) - .build(); - - this.httpClient = HttpClientBuilder.create() - .setDefaultRequestConfig(config) - .disableCookieManagement() - .build(); + private String buildUrl(String baseUrl, String path) { + return normalizeBaseUrl(baseUrl) + API_PREFIX + path; } private String normalizeBaseUrl(String baseUrl) { if (baseUrl == null) { - throw new IllegalArgumentException("PowerDNS base URL cannot be null"); + throw new IllegalArgumentException("Base URL cannot be null"); } - String normalizedUrl = baseUrl.trim(); - if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) { - normalizedUrl = "http://" + normalizedUrl; + String url = baseUrl.trim(); + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "http://" + url; } - if (normalizedUrl.endsWith("/")) { - normalizedUrl = normalizedUrl.substring(0, normalizedUrl.length() - 1); + if (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); } - return normalizedUrl; + return url; } - private String formatZoneName(String zoneName) { + private String normalizeZone(String zoneName) { + if (StringUtils.isBlank(zoneName)) { + throw new IllegalArgumentException("Zone name must not be null or empty"); + } String zone = zoneName.trim().toLowerCase(); if (!zone.endsWith(".")) { - zone += "."; + zone = zone + "."; + } + if (zone.length() < 2) { + throw new IllegalArgumentException("Zone name is too short"); } return zone; } - private String formatRecordName(String recordName, String zoneName) { + String normalizeRecordName(String recordName, String zoneName) { if (recordName == null) { - throw new IllegalArgumentException("Record name cannot be null"); + throw new IllegalArgumentException("Record name must not be null"); } - String normalizedZone = formatZoneName(zoneName); - String zoneWithoutDot = normalizedZone.substring(0, normalizedZone.length() - 1); - + String normalizedZone = normalizeZone(zoneName); String name = recordName.trim().toLowerCase(); - - // Root record + // Apex of the zone if (name.equals("@") || name.isEmpty()) { return normalizedZone; } - // Already absolute + String zoneWithoutDot = normalizedZone.substring(0, normalizedZone.length() - 1); + // Already absolute (ends with dot) if (name.endsWith(".")) { + // Check if the record belongs to the zone + if (!name.equals(normalizedZone) && !name.endsWith("." + zoneWithoutDot + ".")) { + throw new IllegalArgumentException( + String.format("Record '%s' does not belong to zone '%s'", recordName, zoneName) + ); + } return name; } - - // Fully qualified but missing trailing dot - if (name.equals(zoneWithoutDot) || name.endsWith("." + zoneWithoutDot)) { + if (name.contains(".")) { return name + "."; } - - // Relative name + // Relative name → append zone return name + "." + normalizedZone; } - private String buildApiUrl(String baseUrl, String path) { - return normalizeBaseUrl(baseUrl) + "/api/v1" + path; - } - @Override public void close() { try { 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 8af0aa9806c9..9570a1e5f95f 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 @@ -26,6 +26,7 @@ import org.apache.cloudstack.dns.DnsRecord; import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.dns.DnsZone; +import org.apache.cloudstack.dns.exception.DnsProviderException; import com.cloud.utils.StringUtils; import com.cloud.utils.component.AdapterBase; @@ -40,54 +41,65 @@ public DnsProviderType getProviderType() { return DnsProviderType.PowerDNS; } - public void validate(DnsServer server) { + public void validate(DnsServer server) throws DnsProviderException { validateServerParams(server); client.validate(server.getUrl(), server.getApiKey()); } @Override - public String provisionZone(DnsServer server, DnsZone zone) { + public String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException { validateServerZoneParams(server, zone); - return client.createZone(server.getUrl(), server.getApiKey(), zone.getName(), server.getNameServers()); + return client.createZone(server.getUrl(), + server.getApiKey(), + zone.getName(), + "Native", + false, + server.getNameServers() + ); } @Override - public void deleteZone(DnsServer server, DnsZone zone) { + public void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException { validateServerZoneParams(server, zone); client.deleteZone(server.getUrl(), server.getApiKey(), zone.getName()); } + @Override + public void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException { + validateServerZoneParams(server, zone); + client.updateZone(server.getUrl(), server.getApiKey(), zone.getName(), "Native", false, server.getNameServers()); + } + public enum ChangeType { REPLACE, DELETE } @Override - public void addRecord(DnsServer server, DnsZone zone, DnsRecord record) { + public String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { validateServerZoneParams(server, zone); - applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.REPLACE); + return applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.REPLACE); } @Override - public void updateRecord(DnsServer server, DnsZone zone, DnsRecord record) { - addRecord(server, zone, record); + public String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { + validateServerZoneParams(server, zone); + return addRecord(server, zone, record); } - @Override - public void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) { + public void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { validateServerZoneParams(server, zone); applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.DELETE); } - public void applyRecord(String serverUrl, String apiKey, String zoneName, DnsRecord record, ChangeType changeType) { - client.modifyRecord(serverUrl, apiKey, zoneName, record.getName(), record.getType().name(), + public String applyRecord(String serverUrl, String apiKey, String zoneName, DnsRecord record, ChangeType changeType) throws DnsProviderException { + return client.modifyRecord(serverUrl, apiKey, zoneName, record.getName(), record.getType().name(), record.getTtl(), record.getContents(), changeType.name()); } - - @Override - public List listRecords(DnsServer server, DnsZone zone) { + public List listRecords(DnsServer server, DnsZone zone) throws DnsProviderException { + validateServerZoneParams(server, zone); List records = new ArrayList<>(); for (JsonNode rrset: client.listRecords(server.getUrl(), server.getApiKey(), zone.getName())) { String name = rrset.path("name").asText(); @@ -114,7 +126,7 @@ public List listRecords(DnsServer server, DnsZone zone) { return records; } - void validateServerZoneParams(DnsServer server, DnsZone zone) { + void validateServerZoneParams(DnsServer server, DnsZone zone) throws DnsProviderException { validateServerParams(server); if (StringUtils.isBlank(zone.getName())) { throw new IllegalArgumentException("Zone name cannot be empty"); @@ -130,8 +142,6 @@ void validateServerParams(DnsServer server) { } } - - @Override public boolean configure(String name, Map params) { if (client == null) { diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java new file mode 100644 index 000000000000..8c19568bb463 --- /dev/null +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java @@ -0,0 +1,84 @@ +// 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.powerdns; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.mockito.InjectMocks; + +public class PowerDnsClientTest { + @InjectMocks + PowerDnsClient client = new PowerDnsClient(); + + @Test + public void testNormalizeApexRecord() { + String result = client.normalizeRecordName("@", "example.com"); + assertEquals("example.com.", result); + + result = client.normalizeRecordName("", "example.com"); + assertEquals("example.com.", result); + } + + @Test + public void testNormalizeRelativeRecord() { + String result = client.normalizeRecordName("www", "example.com"); + assertEquals("www.example.com.", result); + + result = client.normalizeRecordName("WWW", "example.com"); // test case-insensitive + assertEquals("www.example.com.", result); + } + + @Test + public void testNormalizeAbsoluteRecordWithinZone() { + String result = client.normalizeRecordName("www.example.com.", "example.com"); + assertEquals("www.example.com.", result); + } + + @Test(expected = IllegalArgumentException.class) + public void testNormalizeAbsoluteRecordOutsideZoneThrows() { + client.normalizeRecordName("other.com.", "example.com"); + } + + @Test + public void testNormalizeDottedNameWithoutTrailingDot() { + String result = client.normalizeRecordName("api.test.com", "example.com"); + assertEquals("api.test.com.", result); + } + + @Test + public void testNormalizeRelativeSubdomain() { + String result = client.normalizeRecordName("mail", "example.com"); + assertEquals("mail.example.com.", result); + } + + @Test(expected = IllegalArgumentException.class) + public void testNormalizeNullRecordNameThrows() { + client.normalizeRecordName(null, "example.com"); + } + + @Test + public void testNormalizeZoneNormalization() { + String result = client.normalizeRecordName("www", "Example.Com"); + assertEquals("www.example.com.", result); + + result = client.normalizeRecordName("www", "example.com."); + assertEquals("www.example.com.", result); + } + +} \ No newline at end of file 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 7dedc3a8f16e..57e4cd14d4cb 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -61,6 +61,7 @@ import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; import com.cloud.utils.db.Filter; @@ -161,9 +162,7 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { } Account caller = CallContext.current().getCallingAccount(); - if (!accountMgr.isRootAdmin(caller.getId()) && dnsServer.getAccountId() != caller.getId()) { - throw new PermissionDeniedException("You do not have permission to update this DNS server."); - } + accountMgr.checkAccess(caller, null, true, dnsServer); boolean validationRequired = false; String originalUrl = dnsServer.getUrl(); @@ -234,9 +233,7 @@ public boolean deleteDnsServer(DeleteDnsServerCmd cmd) { throw new InvalidParameterValueException(String.format("DNS server with ID: %s not found.", dnsServerId)); } Account caller = CallContext.current().getCallingAccount(); - if (!accountMgr.isRootAdmin(caller.getId()) && dnsServer.getAccountId() != caller.getId()) { - throw new PermissionDeniedException("You do not have permission to delete this DNS server."); - } + accountMgr.checkAccess(caller, null, true, dnsServer); return dnsServerDao.remove(dnsServerId); } @@ -248,6 +245,7 @@ public DnsServerResponse createDnsServerResponse(DnsServer server) { response.setUrl(server.getUrl()); response.setProvider(server.getProviderType()); response.setPublic(server.isPublic()); + response.setNameServers(server.getNameServers()); response.setObjectName("dnsserver"); return response; } @@ -270,8 +268,8 @@ public boolean deleteDnsZone(Long zoneId) { if (server != null && zone.getState() == DnsZone.State.Active) { try { DnsProvider provider = getProvider(server.getProviderType()); - logger.debug("Deleting DNS zone: {} from provider.", zone.getName()); provider.deleteZone(server, zone); + logger.debug("Deleted DNS zone: {}", zone.getName()); } catch (Exception ex) { logger.error("Failed to delete DNS zone from provider", ex); throw new CloudRuntimeException("Failed to delete DNS zone."); @@ -287,26 +285,32 @@ public DnsZone getDnsZone(Long id) { @Override public DnsZone updateDnsZone(UpdateDnsZoneCmd cmd) { - DnsZoneVO zone = dnsZoneDao.findById(cmd.getId()); - if (zone == null) { + DnsZoneVO dnsZone = dnsZoneDao.findById(cmd.getId()); + if (dnsZone == null) { throw new InvalidParameterValueException("DNS zone not found."); } - - // ACL Check Account caller = CallContext.current().getCallingAccount(); - accountMgr.checkAccess(caller, null, true, zone); - - // Update fields + accountMgr.checkAccess(caller, null, true, dnsZone); boolean updated = false; if (cmd.getDescription() != null) { - zone.setDescription(cmd.getDescription()); + dnsZone.setDescription(cmd.getDescription()); updated = true; } if (updated) { - dnsZoneDao.update(zone.getId(), zone); + DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId()); + if (server == null) { + throw new CloudRuntimeException("The underlying DNS server for this DNS zone is missing."); + } + try { + DnsProvider provider = getProvider(server.getProviderType()); + provider.updateZone(server, dnsZone); + } catch (Exception ex) { + logger.error("Failed to update DNS zone: {} on DNS server: {}", dnsZone.getName(), server.getName(), ex); + throw new CloudRuntimeException("Failed to update DNS zone: " + dnsZone.getName()); + } } - return zone; + return dnsZone; } @Override @@ -328,11 +332,6 @@ public ListResponse listDnsZones(ListDnsZonesCmd cmd) { return response; } - @Override - public DnsZone getDnsZone(long id) { - return null; - } - @Override public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); @@ -344,10 +343,10 @@ public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { accountMgr.checkAccess(caller, null, true, zone); DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); try { - DnsRecord record = new DnsRecord(cmd.getName(), cmd.getType(), cmd.getContents(), cmd.getTtl()); DnsProvider provider = getProvider(server.getProviderType()); - // Add Record via Provider - provider.addRecord(server, zone, record); + DnsRecord record = new DnsRecord(cmd.getName(), cmd.getType(), cmd.getContents(), cmd.getTtl()); + String normalizedRecordName = provider.addRecord(server, zone, record); + record.setName(normalizedRecordName); return createDnsRecordResponse(record); } catch (Exception ex) { logger.error("Failed to add DNS record via provider", ex); @@ -449,8 +448,8 @@ public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) { } @Override - public DnsZone provisionDnsZone(long zoneId) { - DnsZoneVO dnsZone = dnsZoneDao.findById(zoneId); + public DnsZone provisionDnsZone(long dnsZoneId) { + DnsZoneVO dnsZone = dnsZoneDao.findById(dnsZoneId); if (dnsZone == null) { throw new CloudRuntimeException("DNS zone not found during provisioning"); } @@ -460,11 +459,11 @@ public DnsZone provisionDnsZone(long zoneId) { String externalReferenceId = provider.provisionZone(server, dnsZone); dnsZone.setExternalReference(externalReferenceId); dnsZone.setState(DnsZone.State.Active); - logger.debug("DNS zone: {} created successfully on DNS server: {} with ID: {}", dnsZone.getName(), server.getName(), zoneId); dnsZoneDao.update(dnsZone.getId(), dnsZone); + logger.debug("DNS zone: {} created successfully on DNS server: {} with ID: {}", dnsZone.getName(), server.getName(), dnsZoneId); } catch (Exception ex) { + dnsZoneDao.remove(dnsZoneId); logger.error("Failed to provision DNS zone: {} on DNS server: {}", dnsZone.getName(), server.getName(), ex); - dnsZoneDao.remove(zoneId); throw new CloudRuntimeException("Failed to provision DNS zone: " + dnsZone.getName()); } return dnsZone; @@ -570,6 +569,10 @@ private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boo networkId = nic != null ? nic.getNetworkId() : null; } + // networkId may not be of Shared network type + // there might be multiple shared networks + // possible to have dns record for secondary ip + if (nic == null) { throw new CloudRuntimeException("No valid NIC found for this Instance on the specified Network."); } @@ -592,7 +595,7 @@ private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boo // Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain") String recordName = String.valueOf(instance.getInstanceName()); - if (map.getSubDomain() != null && !map.getSubDomain().isEmpty()) { + if (StringUtils.isNotBlank(map.getSubDomain() )) { recordName = recordName + "." + map.getSubDomain(); } @@ -664,6 +667,7 @@ public List> getCommands() { cmdList.add(DeleteDnsZoneCmd.class); cmdList.add(UpdateDnsZoneCmd.class); cmdList.add(AssociateDnsZoneToNetworkCmd.class); + cmdList.add(DisassociateDnsZoneFromNetworkCmd.class); // DNS Record Commands cmdList.add(CreateDnsRecordCmd.class); diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java index a200cb988923..45fb81d64f4b 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java @@ -17,6 +17,8 @@ package org.apache.cloudstack.dns.vo; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.UUID; @@ -34,7 +36,9 @@ import org.apache.cloudstack.dns.DnsProviderType; import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.DnsZone; +import com.cloud.utils.StringUtils; import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; @@ -79,6 +83,9 @@ public class DnsServerVO implements DnsServer { @Column(name = "account_id") private long accountId; + @Column(name = "domain_id") + private long domainId; + @Column(name = "name_servers") private String nameServers; @@ -116,6 +123,11 @@ public long getId() { return id; } + @Override + public Class getEntityType() { + return DnsZone.class; + } + @Override public String getName() { return name; @@ -206,7 +218,15 @@ public void setName(String name) { this.name = name; } - public String getNameServers() { - return nameServers; + public List getNameServers() { + if (StringUtils.isBlank(nameServers)) { + return Collections.emptyList(); + } + return Arrays.asList(nameServers.split(",")); + } + + @Override + public long getDomainId() { + return domainId; } } From c64cf81db342261c1498e3cd6ffaf64bf6ed6846 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Fri, 20 Feb 2026 12:45:21 +0530 Subject: [PATCH 12/23] include: 1. port for dns_server 2. remove hard coded localhost for server_id 3. resolve and store server id if not passed in the api request 4. restrict public dns_server for domain admins and admins --- .../apache/cloudstack/api/ApiConstants.java | 2 + .../api/command/user/dns/AddDnsServerCmd.java | 17 ++- .../api/response/DnsServerResponse.java | 6 +- .../api/response/DnsZoneResponse.java | 8 -- .../apache/cloudstack/dns/DnsProvider.java | 2 + .../org/apache/cloudstack/dns/DnsServer.java | 6 + .../META-INF/db/schema-42210to42300.sql | 2 + .../dns/powerdns/PowerDnsClient.java | 105 +++++++++++++----- .../dns/powerdns/PowerDnsProvider.java | 85 +++++++++----- .../dns/DnsProviderManagerImpl.java | 23 +++- .../apache/cloudstack/dns/vo/DnsServerVO.java | 34 +++++- 11 files changed, 210 insertions(+), 80 deletions(-) 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 605ff28d50e0..13dd0305a003 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1335,6 +1335,7 @@ public class ApiConstants { // DNS provider related public static final String NAME_SERVERS = "nameservers"; + public static final String DNS_USER_NAME = "dnsusername"; public static final String CREDENTIALS = "credentials"; public static final String DNS_ZONE_ID = "dnszoneid"; public static final String DNS_SERVER_ID = "dnsserverid"; @@ -1351,6 +1352,7 @@ public class ApiConstants { public static final String X_API_KEY = "X-API-Key"; public static final String DISABLED = "disabled"; public static final String CONTENT_TYPE = "Content-Type"; + public static final String NATIVE_ZONE = "Native"; 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/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java index 126aafba6420..7f2148d014e5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -53,7 +53,11 @@ public class AddDnsServerCmd extends BaseCmd { @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") private String provider; - @Parameter(name = ApiConstants.CREDENTIALS, type = CommandType.STRING, description = "API Key or Credentials for the external provider") + @Parameter(name = ApiConstants.DNS_USER_NAME, type = CommandType.STRING, + description = "Username or email associated with the external DNS provider account (used for authentication)") + private String dnsUserName; + + @Parameter(name = ApiConstants.CREDENTIALS, required = true, type = CommandType.STRING, description = "API key or credentials for the external provider") private String credentials; @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") @@ -69,6 +73,9 @@ public class AddDnsServerCmd extends BaseCmd { required = true, description = "Comma separated list of name servers") private List nameServers; + @Parameter(name = "externalserverid", type = CommandType.STRING, description = "External server id or hostname for the DNS server, e.g., 'localhost' for PowerDNS") + private String externalServerId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -117,4 +124,12 @@ public void execute() { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); } } + + public String getExternalServerId() { + return externalServerId; + } + + public String getDnsUserName() { + return dnsUserName; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java index 57f6ce28d8f8..f96e05820f4e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java @@ -55,10 +55,12 @@ public class DnsServerResponse extends BaseResponse { @Param(description = "Is the DNS server publicly available") private Boolean isPublic; - @SerializedName(ApiConstants.PUBLIC_DOMAIN_SUFFIX) @Param(description = "The public domain suffix for the DNS server") + @SerializedName(ApiConstants.PUBLIC_DOMAIN_SUFFIX) + @Param(description = "The public domain suffix for the DNS server") private String publicDomainSuffix; - @SerializedName(ApiConstants.NAME_SERVERS) @Param(description = "Name servers entries associated to DNS server") + @SerializedName(ApiConstants.NAME_SERVERS) + @Param(description = "Name servers entries associated to DNS server") private List nameServers; public DnsServerResponse() { diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java index a2b19d32fa19..4bad0b513ea4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java @@ -39,10 +39,6 @@ public class DnsZoneResponse extends BaseResponse { @Param(description = "ID of the DNS server this zone belongs to") private Long dnsServerId; - @SerializedName("dnsservername") - @Param(description = "Name of the DNS server this zone belongs to") - private String dnsServerName; - @SerializedName(ApiConstants.NETWORK_ID) @Param(description = "ID of the network this zone is associated with") private String networkId; @@ -76,10 +72,6 @@ public void setDnsServerId(Long dnsServerId) { this.dnsServerId = dnsServerId; } - public void setDnsServerName(String dnsServerName) { - this.dnsServerName = dnsServerName; - } - public void setNetworkId(String networkId) { this.networkId = networkId; } 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 7d4ab1133b72..852df8dd0bf7 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -29,6 +29,8 @@ public interface DnsProvider extends Adapter { // Validates connectivity to the server void validate(DnsServer server) throws Exception; + String validateAndResolveServer(DnsServer server) throws Exception; + // Zone Operations String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException; void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException; diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java index 14160cdf1ffc..49f7ddea5b4a 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java @@ -46,4 +46,10 @@ enum State { Date getCreated(); Date getRemoved(); + + String getPublicDomainSuffix(); + + String getExternalServerId(); + + Integer getPort(); } 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 208991bf4b38..244930676c1b 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 @@ -62,7 +62,9 @@ CREATE TABLE `cloud`.`dns_server` ( `name` varchar(255) NOT NULL COMMENT 'display name of the dns server', `provider_type` varchar(255) NOT NULL COMMENT 'Provider type such as PowerDns', `url` varchar(1024) NOT NULL COMMENT 'dns server url', + `dns_username` varchar(255) NOT NULL COMMENT 'username or email for dns server credentials', `api_key` varchar(255) NOT NULL COMMENT 'dns server api_key', + `dns_server_name` varchar(255) COMMENT 'dns server name e.g. localhost for powerdns', `port` int(11) DEFAULT NULL COMMENT 'optional dns server port', `name_servers` varchar(1024) DEFAULT NULL COMMENT 'Comma separated list of name servers', `is_public` tinyint(1) NOT NULL DEFAULT '0', 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 28be4f5155ec..2a19c306a088 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 @@ -18,6 +18,8 @@ package org.apache.cloudstack.dns.powerdns; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Collections; @@ -62,7 +64,7 @@ public class PowerDnsClient implements AutoCloseable { private static final int MAX_CONNECTIONS_TOTAL = 50; private static final int MAX_CONNECTIONS_PER_ROUTE = 10; private static final String API_PREFIX = "/api/v1"; - private static final String DEFAULT_SERVER = "localhost"; + public static final String DEFAULT_SERVER_NAME = "localhost"; private final CloseableHttpClient httpClient; @@ -85,30 +87,59 @@ public PowerDnsClient() { .build(); } - public void validate(String baseUrl, String apiKey) throws DnsProviderException { - String url = buildUrl(baseUrl, "/servers"); + public String resolveServerId(String baseUrl, Integer port, String apiKey, String externalServerId) throws DnsProviderException { + if (StringUtils.isNotBlank(externalServerId)) { + return validateServerId(baseUrl, port, apiKey, externalServerId); + } + return discoverAuthoritativeServerId(baseUrl, port, apiKey); + } + + public String validateServerId(String baseUrl, Integer port, String apiKey, String externalServerId) throws DnsProviderException { + String encodedServer = URLEncoder.encode(externalServerId, StandardCharsets.UTF_8); + HttpGet request = new HttpGet(buildUrl(baseUrl, port, "/servers/" + encodedServer)); + JsonNode server = execute(request, apiKey, 200); + if (!ApiConstants.AUTHORITATIVE.equalsIgnoreCase(server.path("daemon_type").asText(null))) { + throw new DnsOperationException(String.format("Server %s is not authoritative type=%s", externalServerId, + server.path("daemon_type").asText(null))); + } + return externalServerId; + } + + public String discoverAuthoritativeServerId(String baseUrl, Integer port, String apiKey) throws DnsProviderException { + String url = buildUrl(baseUrl, port , "/servers"); HttpGet request = new HttpGet(url); JsonNode servers = execute(request, apiKey, 200); if (servers == null || !servers.isArray() || servers.isEmpty()) { throw new DnsOperationException("No servers returned by PowerDNS API"); } - boolean authoritativeFound = false; + String fallbackId = null; for (JsonNode server : servers) { - if (ApiConstants.AUTHORITATIVE.equalsIgnoreCase(server.path("daemon_type").asText(null))) { - authoritativeFound = true; - break; + String daemonType = server.path("daemon_type").asText(null); + if (!ApiConstants.AUTHORITATIVE.equalsIgnoreCase(daemonType)) { + continue; + } + String serverId = server.path(ApiConstants.ID).asText(null); + if (StringUtils.isBlank(serverId)) { + continue; + } + // Prefer localhost if present + if (DEFAULT_SERVER_NAME.equals(serverId)) { + return serverId; + } + if (fallbackId == null) { + fallbackId = serverId; } } - if (!authoritativeFound) { - throw new DnsOperationException("No authoritative PowerDNS server found"); + if (fallbackId != null) { + return fallbackId; } + throw new DnsOperationException("No authoritative PowerDNS server found"); } - public String createZone(String baseUrl, String apiKey, String zoneName, String zoneKind, boolean dnsSecFlag, - List nameServers) throws DnsProviderException { - - validate(baseUrl, apiKey); + public String createZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName, + String zoneKind, boolean dnsSecFlag, List nameServers) throws DnsProviderException { + validateServerId(baseUrl, port, externalServerId, apiKey); String normalizedZone = normalizeZone(zoneName); ObjectNode json = MAPPER.createObjectNode(); json.put(ApiConstants.NAME, normalizedZone); @@ -120,7 +151,7 @@ public String createZone(String baseUrl, String apiKey, String zoneName, String nsArray.add(ns.endsWith(".") ? ns : ns + "."); } } - HttpPost request = new HttpPost(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones")); + HttpPost request = new HttpPost(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones")); request.setEntity(new StringEntity(json.toString(), StandardCharsets.UTF_8)); JsonNode response = execute(request, apiKey, 201); if (response == null) { @@ -133,15 +164,15 @@ public String createZone(String baseUrl, String apiKey, String zoneName, String return zoneId; } - public void updateZone(String baseUrl, String apiKey, String zoneName, String zoneKind, Boolean dnsSecFlag, - List nameServers) throws DnsProviderException { + public void updateZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName, + String zoneKind, Boolean dnsSecFlag, List nameServers) throws DnsProviderException { + validateServerId(baseUrl, port, externalServerId, apiKey); String normalizedZone = normalizeZone(zoneName); String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); - String url = buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone); + String url = buildUrl(baseUrl, port,"/servers/" + externalServerId + "/zones/" + encodedZone); ObjectNode json = MAPPER.createObjectNode(); - if (dnsSecFlag != null) { json.put(ApiConstants.DNS_SEC, dnsSecFlag); } @@ -159,18 +190,18 @@ public void updateZone(String baseUrl, String apiKey, String zoneName, String zo execute(request, apiKey, 204); } - public void deleteZone(String baseUrl, String apiKey, String zoneName) throws DnsProviderException { - validate(baseUrl, apiKey); + public void deleteZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName) throws DnsProviderException { + validateServerId(baseUrl, port, apiKey, externalServerId); String normalizedZone = normalizeZone(zoneName); String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); - HttpDelete request = new HttpDelete(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone)); + HttpDelete request = new HttpDelete(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone)); execute(request, apiKey, 204, 404); } - public String modifyRecord(String baseUrl, String apiKey, String zoneName, String recordName, String type, long ttl, - List contents, String changeType) throws DnsProviderException { + public String modifyRecord(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName, + String recordName, String type, long ttl, List contents, String changeType) throws DnsProviderException { - validate(baseUrl, apiKey); + validateServerId(baseUrl, port, apiKey, externalServerId); String normalizedZone = normalizeZone(zoneName); String normalizedRecord = normalizeRecordName(recordName, normalizedZone); ObjectNode root = MAPPER.createObjectNode(); @@ -189,17 +220,17 @@ public String modifyRecord(String baseUrl, String apiKey, String zoneName, Strin } } String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); - HttpPatch request = new HttpPatch(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone)); + HttpPatch request = new HttpPatch(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone)); request.setEntity(new org.apache.http.entity.StringEntity(root.toString(), StandardCharsets.UTF_8)); execute(request, apiKey, 204); return normalizedRecord; } - public Iterable listRecords(String baseUrl, String apiKey, String zoneName) throws DnsProviderException { - validate(baseUrl, apiKey); + public Iterable listRecords(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName) throws DnsProviderException { + validateServerId(baseUrl, port, apiKey, externalServerId); String normalizedZone = normalizeZone(zoneName); String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); - HttpGet request = new HttpGet(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone)); + HttpGet request = new HttpGet(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone)); JsonNode zoneNode = execute(request, apiKey, 200); if (zoneNode == null || !zoneNode.has(ApiConstants.RR_SETS)) { return Collections.emptyList(); @@ -239,8 +270,22 @@ private JsonNode execute(HttpUriRequest request, String apiKey, int... expectedS } } - private String buildUrl(String baseUrl, String path) { - return normalizeBaseUrl(baseUrl) + API_PREFIX + path; + private String buildUrl(String baseUrl, Integer port, String path) { + String fullUrl = normalizeBaseUrl(baseUrl); + if (port != null && port > 0) { + try { + URI uri = new URI(fullUrl); + if (uri.getPort() == -1) { + fullUrl = fullUrl + ":" + port; + } + } catch (URISyntaxException e) { + fullUrl = fullUrl + ":" + port; + } + } + if (!path.startsWith("/")) { + path = "/" + path; + } + return fullUrl + API_PREFIX + path; } private String normalizeBaseUrl(String baseUrl) { 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 9570a1e5f95f..44862ff2e254 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 @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.dns.DnsProvider; import org.apache.cloudstack.dns.DnsProviderType; import org.apache.cloudstack.dns.DnsRecord; @@ -42,32 +43,44 @@ public DnsProviderType getProviderType() { } public void validate(DnsServer server) throws DnsProviderException { - validateServerParams(server); - client.validate(server.getUrl(), server.getApiKey()); + validateRequiredServerFields(server); + client.validateServerId(server.getUrl(), server.getPort(), server.getApiKey(), server.getExternalServerId()); + } + + @Override + public String validateAndResolveServer(DnsServer server) throws Exception { + validateRequiredServerFields(server); + return client.resolveServerId(server.getUrl(), server.getPort(), server.getApiKey(), server.getExternalServerId()); } @Override public String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException { - validateServerZoneParams(server, zone); - return client.createZone(server.getUrl(), + validateRequiredServerAndZoneFields(server, zone); + return client.createZone( + server.getUrl(), + server.getPort(), server.getApiKey(), + server.getExternalServerId(), zone.getName(), - "Native", - false, - server.getNameServers() + ApiConstants.NATIVE_ZONE, false, server.getNameServers() ); } @Override public void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException { - validateServerZoneParams(server, zone); - client.deleteZone(server.getUrl(), server.getApiKey(), zone.getName()); + validateRequiredServerAndZoneFields(server, zone); + client.deleteZone(server.getUrl(), server.getPort(), server.getApiKey(), server.getExternalServerId(), zone.getName()); } @Override public void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException { - validateServerZoneParams(server, zone); - client.updateZone(server.getUrl(), server.getApiKey(), zone.getName(), "Native", false, server.getNameServers()); + validateRequiredServerAndZoneFields(server, zone); + client.updateZone( + server.getUrl(), + server.getPort(), + server.getApiKey(), + server.getExternalServerId(), + zone.getName(), ApiConstants.NATIVE_ZONE, false, server.getNameServers()); } public enum ChangeType { @@ -76,42 +89,56 @@ public enum ChangeType { @Override public String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { - validateServerZoneParams(server, zone); - return applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.REPLACE); + validateRequiredServerAndZoneFields(server, zone); + return applyRecord( + server.getUrl(), + server.getPort(), + server.getApiKey(), + server.getExternalServerId(), + zone.getName(), record, ChangeType.REPLACE); } @Override public String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { - validateServerZoneParams(server, zone); + validateRequiredServerAndZoneFields(server, zone); return addRecord(server, zone, record); } @Override public void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { - validateServerZoneParams(server, zone); - applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.DELETE); + validateRequiredServerAndZoneFields(server, zone); + applyRecord(server.getUrl(), + server.getPort(), + server.getApiKey(), + server.getExternalServerId(), + zone.getName(), record, ChangeType.DELETE); } - public String applyRecord(String serverUrl, String apiKey, String zoneName, DnsRecord record, ChangeType changeType) throws DnsProviderException { - return client.modifyRecord(serverUrl, apiKey, zoneName, record.getName(), record.getType().name(), - record.getTtl(), record.getContents(), changeType.name()); + public String applyRecord(String serverUrl, Integer port, String apiKey, String externalServerId, String zoneName, + DnsRecord record, ChangeType changeType) throws DnsProviderException { + + return client.modifyRecord(serverUrl, port, apiKey, externalServerId, zoneName, record.getName(), + record.getType().name(), record.getTtl(), record.getContents(), changeType.name()); } @Override public List listRecords(DnsServer server, DnsZone zone) throws DnsProviderException { - validateServerZoneParams(server, zone); + validateRequiredServerAndZoneFields(server, zone); List records = new ArrayList<>(); - for (JsonNode rrset: client.listRecords(server.getUrl(), server.getApiKey(), zone.getName())) { - String name = rrset.path("name").asText(); - String typeStr = rrset.path("type").asText(); - int ttl = rrset.path("ttl").asInt(0); + Iterable rrsetNodes = client.listRecords(server.getUrl(), server.getPort(), server.getApiKey(), + server.getExternalServerId(), zone.getName()); + + for (JsonNode rrset : rrsetNodes) { + String name = rrset.path(ApiConstants.NAME).asText(); + String typeStr = rrset.path(ApiConstants.TYPE).asText(); + int ttl = rrset.path(ApiConstants.TTL).asInt(0); if (!"SOA".equalsIgnoreCase(typeStr)) { try { List contents = new ArrayList<>(); - JsonNode recordsNode = rrset.path("records"); + JsonNode recordsNode = rrset.path(ApiConstants.RECORDS); if (recordsNode.isArray()) { for (JsonNode rec : recordsNode) { - String content = rec.path("content").asText(); + String content = rec.path(ApiConstants.CONTENT).asText(); if (!content.isEmpty()) { contents.add(content); } @@ -126,14 +153,14 @@ public List listRecords(DnsServer server, DnsZone zone) throws DnsPro return records; } - void validateServerZoneParams(DnsServer server, DnsZone zone) throws DnsProviderException { - validateServerParams(server); + void validateRequiredServerAndZoneFields(DnsServer server, DnsZone zone) { + validateRequiredServerFields(server); if (StringUtils.isBlank(zone.getName())) { throw new IllegalArgumentException("Zone name cannot be empty"); } } - void validateServerParams(DnsServer server) { + void validateRequiredServerFields(DnsServer server) { if (StringUtils.isBlank(server.getUrl())) { throw new IllegalArgumentException("PowerDNS API URL cannot be empty"); } 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 57e4cd14d4cb..6739ccb0b11c 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -109,17 +109,29 @@ public DnsServer addDnsServer(AddDnsServerCmd cmd) { throw new InvalidParameterValueException( "This Account already has a DNS server integration for URL: " + cmd.getUrl()); } + + boolean isDnsPublic = cmd.isPublic(); + String publicDomainSuffix = cmd.getPublicDomainSuffix(); + if (caller.getType().equals(Account.Type.NORMAL)) { + logger.info("Only admin and domain admin users are allowed to configure a public DNS server"); + isDnsPublic = false; + publicDomainSuffix = null; + } DnsProviderType type = DnsProviderType.fromString(cmd.getProvider()); - DnsProvider provider = getProvider(type); - DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), type, cmd.getCredentials(), cmd.getPort(), - cmd.isPublic(), cmd.getPublicDomainSuffix(), cmd.getNameServers(), caller.getId()); + DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), cmd.getPort(), cmd.getExternalServerId(), type, + cmd.getDnsUserName(), cmd.getCredentials(), isDnsPublic, publicDomainSuffix, cmd.getNameServers(), + caller.getAccountId(), caller.getDomainId()); try { - provider.validate(server); + DnsProvider provider = getProvider(type); + String dnsServerId = provider.validateAndResolveServer(server); // localhost for PowerDNS + if (StringUtils.isNotBlank(dnsServerId)) { + server.setExternalServerId(dnsServerId); + } + return dnsServerDao.persist(server); } catch (Exception ex) { logger.error("Failed to validate DNS server", ex); throw new CloudRuntimeException("Failed to validate DNS server"); } - return dnsServerDao.persist(server); } @Override @@ -246,6 +258,7 @@ public DnsServerResponse createDnsServerResponse(DnsServer server) { response.setProvider(server.getProviderType()); response.setPublic(server.isPublic()); response.setNameServers(server.getNameServers()); + response.setPublicDomainSuffix(server.getPublicDomainSuffix()); response.setObjectName("dnsserver"); return response; } diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java index 45fb81d64f4b..2b922f1063e4 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java @@ -44,7 +44,7 @@ @Entity @Table(name = "dns_server") -public class DnsServerVO implements DnsServer { +public class DnsServerVO implements DnsServer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @@ -66,10 +66,16 @@ public class DnsServerVO implements DnsServer { @Enumerated(EnumType.STRING) private DnsProviderType providerType; + @Column(name = "dns_user_name") + private String dnsUserName; + @Encrypt @Column(name = "api_key") private String apiKey; + @Column(name = "external_server_id") + private String externalServerId; + @Column(name = "is_public") private boolean isPublic; @@ -91,7 +97,7 @@ public class DnsServerVO implements DnsServer { @Column(name = GenericDao.CREATED_COLUMN) @Temporal(value = TemporalType.TIMESTAMP) - private Date created = null; + private Date created; @Column(name = GenericDao.REMOVED_COLUMN) @Temporal(value = TemporalType.TIMESTAMP) @@ -102,16 +108,18 @@ public class DnsServerVO implements DnsServer { this.created = new Date(); } - public DnsServerVO(String name, String url, DnsProviderType providerType, String apiKey, - Integer port, boolean isPublic, String publicDomainSuffix, List nameServers, - long accountId) { + public DnsServerVO(String name, String url, Integer port, String externalServerId, DnsProviderType providerType, String dnsUserName, String apiKey, + boolean isPublic, String publicDomainSuffix, List nameServers, Long accountId, Long domainId) { this(); this.name = name; this.url = url; this.port = port; + this.externalServerId = externalServerId; this.providerType = providerType; + this.dnsUserName = dnsUserName; this.apiKey = apiKey; this.accountId = accountId; + this.domainId = domainId; this.publicDomainSuffix = publicDomainSuffix; this.isPublic = isPublic; this.state = State.Enabled; @@ -229,4 +237,20 @@ public List getNameServers() { public long getDomainId() { return domainId; } + + public String getPublicDomainSuffix() { + return publicDomainSuffix; + } + + public String getExternalServerId() { + return externalServerId; + } + + public void setExternalServerId(String externalServerId) { + this.externalServerId = externalServerId; + } + + public Integer getPort() { + return this.port; + } } From 4df11a4198254c30167493fee429bd533ee6fbe8 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Sat, 21 Feb 2026 09:25:28 +0530 Subject: [PATCH 13/23] normalize dns zone and record in svc layer, always use dotless data in svc and handle dot version in client --- .../command/user/dns/CreateDnsZoneCmd.java | 24 ++-- .../META-INF/db/schema-42210to42300.sql | 4 +- .../dns/powerdns/PowerDnsClient.java | 4 +- .../apache/cloudstack/dns/DnsUtilTest.java | 103 ++++++++++++++++++ .../dns/DnsProviderManagerImpl.java | 53 +++++---- .../org/apache/cloudstack/dns/DnsUtil.java | 101 +++++++++++++++++ .../apache/cloudstack/dns/vo/DnsServerVO.java | 2 +- 7 files changed, 256 insertions(+), 35 deletions(-) create mode 100644 plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsUtilTest.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/DnsUtil.java diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java index ab0ac9cf4c36..61c7f5b5f44f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java @@ -17,6 +17,8 @@ package org.apache.cloudstack.api.command.user.dns; +import java.util.Arrays; + import javax.inject.Inject; import org.apache.cloudstack.api.APICommand; @@ -27,13 +29,14 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; -import org.apache.cloudstack.api.response.NetworkResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.dns.DnsZone; +import org.apache.commons.lang3.StringUtils; import com.cloud.event.EventTypes; import com.cloud.exception.ResourceAllocationException; +import com.cloud.utils.EnumUtils; @APICommand(name = "createDnsZone", description = "Creates a new DNS Zone on a specific server", responseObject = DnsZoneResponse.class, requestHasSensitiveInfo = false, @@ -55,10 +58,6 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { required = true, description = "The ID of the DNS server to host this zone") private Long dnsServerId; - @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, - description = "Optional: The Guest Network to associate with this zone for auto-registration") - private Long networkId; - @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "The type of zone (Public, Private). Defaults to Public.") private String type; @@ -78,12 +77,15 @@ public Long getDnsServerId() { return dnsServerId; } - public Long getNetworkId() { - return networkId; - } - - public String getType() { - return type; + public DnsZone.ZoneType getType() { + if (StringUtils.isBlank(type)) { + return DnsZone.ZoneType.Public; + } + DnsZone.ZoneType zoneType = EnumUtils.getEnumIgnoreCase(DnsZone.ZoneType.class, type); + if (type == null) { + throw new IllegalArgumentException("Invalid type value, supported values are: " + Arrays.toString(DnsZone.ZoneType.values())); + } + return zoneType; } public String getDescription() { 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 244930676c1b..fe76e949b7c6 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 @@ -62,9 +62,9 @@ CREATE TABLE `cloud`.`dns_server` ( `name` varchar(255) NOT NULL COMMENT 'display name of the dns server', `provider_type` varchar(255) NOT NULL COMMENT 'Provider type such as PowerDns', `url` varchar(1024) NOT NULL COMMENT 'dns server url', - `dns_username` varchar(255) NOT NULL COMMENT 'username or email for dns server credentials', + `dns_username` varchar(255) COMMENT 'username or email for dns server credentials', `api_key` varchar(255) NOT NULL COMMENT 'dns server api_key', - `dns_server_name` varchar(255) COMMENT 'dns server name e.g. localhost for powerdns', + `external_server_id` varchar(255) COMMENT 'dns server id e.g. localhost for powerdns', `port` int(11) DEFAULT NULL COMMENT 'optional dns server port', `name_servers` varchar(1024) DEFAULT NULL COMMENT 'Comma separated list of name servers', `is_public` tinyint(1) NOT NULL DEFAULT '0', 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 2a19c306a088..27a322f7e39d 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 @@ -139,7 +139,7 @@ public String discoverAuthoritativeServerId(String baseUrl, Integer port, String public String createZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName, String zoneKind, boolean dnsSecFlag, List nameServers) throws DnsProviderException { - validateServerId(baseUrl, port, externalServerId, apiKey); + validateServerId(baseUrl, port, apiKey, externalServerId); String normalizedZone = normalizeZone(zoneName); ObjectNode json = MAPPER.createObjectNode(); json.put(ApiConstants.NAME, normalizedZone); @@ -167,7 +167,7 @@ public String createZone(String baseUrl, Integer port, String apiKey, String ext public void updateZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName, String zoneKind, Boolean dnsSecFlag, List nameServers) throws DnsProviderException { - validateServerId(baseUrl, port, externalServerId, apiKey); + validateServerId(baseUrl, port, apiKey, externalServerId); String normalizedZone = normalizeZone(zoneName); String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); String url = buildUrl(baseUrl, port,"/servers/" + externalServerId + "/zones/" + encodedZone); diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsUtilTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsUtilTest.java new file mode 100644 index 000000000000..bc155e8d982a --- /dev/null +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsUtilTest.java @@ -0,0 +1,103 @@ +// 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; + +import static org.apache.cloudstack.dns.DnsUtil.appendPublicSuffixToZone; +import static org.apache.cloudstack.dns.DnsUtil.normalizeDomain; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.logging.log4j.util.Strings; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class DnsUtilTest { + private final String userZoneName; + private final String publicSuffix; + private final String expectedResult; + private final boolean expectException; + + public DnsUtilTest(String userZoneName, + String publicSuffix, + String expectedResult, + boolean expectException) { + this.userZoneName = userZoneName; + this.publicSuffix = publicSuffix; + this.expectedResult = expectedResult; + this.expectException = expectException; + } + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"tenant1.com", "example.com", "tenant1.example.com", false}, + {"dev.tenant2.com", "example.com", "dev.tenant2.example.com", false}, + {"tenant3.example.com", "example.com", "tenant3.example.com", false}, + {"Tenant1.CoM", "ExAmple.CoM", "tenant1.example.com", false}, + {"tenant1.com.", "example.com.", "tenant1.example.com", false}, + {"tenant1.com", "", "tenant1.com", false}, + {"tenant1.com", null, "tenant1.com", false}, + {"test.abc.com", "abc.com", "test.abc.com", false}, + {"sub.test.abc.com", "abc.com", "sub.test.abc.com", false}, + {"test.ai.abc.com", "abc.com", "test.ai.abc.com", false}, + {"deep.sub.abc.com", "abc.com", "deep.sub.abc.com", false}, + {"abc.com", "xyz.com", "abc.xyz.com", false}, + {"test.xyz.com", "xyz.com", "test.xyz.com", false}, + {"test.com.xyz.com", "xyz.com", "test.com.xyz.com", false}, + {"tenant", "example.com", null, true}, // single label + {"test", "abc.com", null, true}, + {"example.com.", "example.com", null, true}, + {"example.com", "example.com", null, true}, // root level forbidden + {"abc.com", "abc.com", null, true}, // root level forbidden + {"tenant1.org", "example.com", null, true}, // TLD mismatch + {"test.ai", "abc.com", null, true}, // TLD mismatch + {null, "example.com", null, true}, + }); + } + + @Test + public void testAppendPublicSuffix() { + if (expectException) { + try { + executeAppendSuffixTest(userZoneName, publicSuffix); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ignored) { + // noop + } + } else { + String result; + if (Strings.isNotBlank(publicSuffix)) { + result = executeAppendSuffixTest(userZoneName, publicSuffix); + } else { + result = appendPublicSuffixToZone(normalizeDomain(userZoneName), publicSuffix); + } + assertEquals(expectedResult, result); + } + } + + String executeAppendSuffixTest(String zoneName, String domainSuffix) { + return appendPublicSuffixToZone( + normalizeDomain(zoneName), + normalizeDomain(domainSuffix)); + } +} 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 6739ccb0b11c..b48eece57aaa 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -117,6 +118,11 @@ public DnsServer addDnsServer(AddDnsServerCmd cmd) { isDnsPublic = false; publicDomainSuffix = null; } + + if (StringUtils.isNotBlank(publicDomainSuffix)) { + publicDomainSuffix = DnsUtil.normalizeDomain(publicDomainSuffix); + } + DnsProviderType type = DnsProviderType.fromString(cmd.getProvider()); DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), cmd.getPort(), cmd.getExternalServerId(), type, cmd.getDnsUserName(), cmd.getCredentials(), isDnsPublic, publicDomainSuffix, cmd.getNameServers(), @@ -208,7 +214,7 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { } if (cmd.getPublicDomainSuffix() != null) { - dnsServer.setPublicDomainSuffix(cmd.getPublicDomainSuffix()); + dnsServer.setPublicDomainSuffix(DnsUtil.normalizeDomain(cmd.getPublicDomainSuffix())); } if (cmd.getNameServers() != null) { @@ -255,6 +261,7 @@ public DnsServerResponse createDnsServerResponse(DnsServer server) { response.setId(server.getUuid()); response.setName(server.getName()); response.setUrl(server.getUrl()); + response.setPort(server.getPort()); response.setProvider(server.getProviderType()); response.setPublic(server.isPublic()); response.setNameServers(server.getNameServers()); @@ -272,7 +279,7 @@ public DnsServer getDnsServer(Long id) { public boolean deleteDnsZone(Long zoneId) { DnsZoneVO zone = dnsZoneDao.findById(zoneId); if (zone == null) { - throw new InvalidParameterValueException("DNS zone with ID " + zoneId + " not found."); + throw new InvalidParameterValueException("DNS zone not found for the given ID."); } Account caller = CallContext.current().getCallingAccount(); @@ -347,23 +354,29 @@ public ListResponse listDnsZones(ListDnsZonesCmd cmd) { @Override public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { + String recordName = StringUtils.trimToEmpty(cmd.getName()).toLowerCase(); + if (StringUtils.isBlank(recordName)) { + throw new InvalidParameterValueException("Empty DNS record name is not allowed"); + } DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); if (zone == 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()); try { + DnsRecord.RecordType type = cmd.getType(); + List normalizedContents = cmd.getContents().stream() + .map(value -> DnsUtil.normalizeDnsRecordValue(value, type)).collect(Collectors.toList()); + DnsRecord record = new DnsRecord(recordName, type, normalizedContents, cmd.getTtl()); DnsProvider provider = getProvider(server.getProviderType()); - DnsRecord record = new DnsRecord(cmd.getName(), cmd.getType(), cmd.getContents(), cmd.getTtl()); String normalizedRecordName = provider.addRecord(server, zone, record); record.setName(normalizedRecordName); return createDnsRecordResponse(record); } catch (Exception ex) { logger.error("Failed to add DNS record via provider", ex); - throw new CloudRuntimeException(String.format("Failed to add DNS record: %s", cmd.getName())); + throw new CloudRuntimeException(String.format("Failed to add DNS record: %s", recordName)); } } @@ -397,7 +410,7 @@ public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) { public ListResponse listDnsRecords(ListDnsRecordsCmd cmd) { DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); if (zone == null) { - throw new InvalidParameterValueException(String.format("DNS zone with ID %s not found.", cmd.getDnsZoneId())); + throw new InvalidParameterValueException("DNS zone not found for the given ID."); } Account caller = CallContext.current().getCallingAccount(); accountMgr.checkAccess(caller, null, true, zone); @@ -435,28 +448,30 @@ public List listProviderNames() { @Override public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) { - Account caller = CallContext.current().getCallingAccount(); + if (StringUtils.isBlank(cmd.getName())) { + throw new InvalidParameterValueException("DNS zone name cannot be empty"); + } + + String dnsZoneName = DnsUtil.normalizeDomain(cmd.getName()); DnsServerVO server = dnsServerDao.findById(cmd.getDnsServerId()); if (server == null) { - throw new InvalidParameterValueException("DNS server not found"); + throw new InvalidParameterValueException(String.format("DNS server not found for the given ID: %s", cmd.getDnsServerId())); } + + Account caller = CallContext.current().getCallingAccount(); boolean isOwner = (server.getAccountId() == caller.getId()); - if (!server.isPublic() && !isOwner) { - throw new PermissionDeniedException("You do not have permission to use this DNS server."); - } - DnsZone.ZoneType type = DnsZone.ZoneType.Public; - if (cmd.getType() != null) { - try { - type = DnsZone.ZoneType.valueOf(cmd.getType()); - } catch (IllegalArgumentException e) { - throw new InvalidParameterValueException("Invalid DNS zone Type"); + if (!isOwner) { + if (!server.isPublic()) { + throw new PermissionDeniedException("You do not have permission to use this DNS server."); } + dnsZoneName = DnsUtil.appendPublicSuffixToZone(dnsZoneName, DnsUtil.normalizeDomain(server.getPublicDomainSuffix())); } - DnsZoneVO existing = dnsZoneDao.findByNameServerAndType(cmd.getName(), server.getId(), type); + DnsZone.ZoneType type = cmd.getType(); + DnsZoneVO existing = dnsZoneDao.findByNameServerAndType(dnsZoneName, server.getId(), type); if (existing != null) { throw new InvalidParameterValueException("DNS zone already exists on this server."); } - DnsZoneVO dnsZoneVO = new DnsZoneVO(cmd.getName(), type, server.getId(), caller.getId(), caller.getDomainId(), cmd.getDescription()); + DnsZoneVO dnsZoneVO = new DnsZoneVO(dnsZoneName, type, server.getId(), caller.getId(), caller.getDomainId(), cmd.getDescription()); return dnsZoneDao.persist(dnsZoneVO); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsUtil.java b/server/src/main/java/org/apache/cloudstack/dns/DnsUtil.java new file mode 100644 index 000000000000..dc312f55287d --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsUtil.java @@ -0,0 +1,101 @@ +// 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; + +import org.apache.commons.validator.routines.DomainValidator; + +import com.cloud.utils.StringUtils; + +public class DnsUtil { + static DomainValidator validator = DomainValidator.getInstance(true); + + public static String appendPublicSuffixToZone(String zoneName, String suffixDomain) { + if (StringUtils.isBlank(suffixDomain)) { + return zoneName; + } + // Already suffixed → return as-is + if (zoneName.toLowerCase().endsWith("." + suffixDomain.toLowerCase())) { + return zoneName; + } + + if (zoneName.equals(suffixDomain)) { + throw new IllegalArgumentException("Cannot create DNS zone at root-level: " + suffixDomain); + } + // Check TLD matches + String tldUser = getTld(zoneName); + String tldSuffix = getTld(suffixDomain); + + if (!tldUser.equalsIgnoreCase(tldSuffix)) { + throw new IllegalArgumentException("TLD mismatch between user zone and domain suffix"); + } + // Remove TLD from userZone + int lastDot = zoneName.lastIndexOf('.'); + String zonePrefix = zoneName.substring(0, lastDot); + return zonePrefix + "." + suffixDomain; + } + + private static String getTld(String domain) { + String[] labels = domain.split("\\."); + return labels[labels.length - 1]; + } + + public static String normalizeDomain(String domain) { + if (StringUtils.isBlank(domain)) { + throw new IllegalArgumentException("Domain cannot be empty"); + } + + String normalized = domain.trim().toLowerCase(); + if (normalized.endsWith(".")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + // Validate domain, allow local/private TLDs + if (!validator.isValid(normalized)) { + throw new IllegalArgumentException("Invalid domain name: " + domain); + } + return normalized; + } + + public static String normalizeDnsRecordValue(String value, DnsRecord.RecordType recordType) { + if (StringUtils.isBlank(value)) { + throw new IllegalArgumentException("DNS record value cannot be empty"); + } + switch (recordType) { + case A: + case AAAA: + // IP addresses: trim only + return value.trim(); + + case CNAME: + case NS: + case PTR: + case SRV: + // Domain names: normalize like zones + return normalizeDomain(value); + case MX: + // PowerDNS MX: contains priority + domain, only trim and lowercase + return value.trim().toLowerCase(); + + case TXT: + // Free text: preserve exactly + return value; + + default: + throw new IllegalArgumentException("Unsupported DNS record type: " + recordType); + } + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java index 2b922f1063e4..d27b3486612b 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java @@ -66,7 +66,7 @@ public class DnsServerVO implements DnsServer { @Enumerated(EnumType.STRING) private DnsProviderType providerType; - @Column(name = "dns_user_name") + @Column(name = "dns_username") private String dnsUserName; @Encrypt From 857436e7f98b979701358f6b31fcc0528562530c Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Sat, 21 Feb 2026 14:36:25 +0530 Subject: [PATCH 14/23] enable apis for all roles --- .../cloudstack/api/command/user/dns/AddDnsServerCmd.java | 4 +++- .../command/user/dns/AssociateDnsZoneToNetworkCmd.java | 5 +++-- .../api/command/user/dns/CreateDnsRecordCmd.java | 3 ++- .../cloudstack/api/command/user/dns/CreateDnsZoneCmd.java | 5 +++-- .../api/command/user/dns/DeleteDnsRecordCmd.java | 5 +++-- .../api/command/user/dns/DeleteDnsServerCmd.java | 5 +++-- .../cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java | 3 ++- .../user/dns/DisassociateDnsZoneFromNetworkCmd.java | 4 +++- .../api/command/user/dns/ListDnsProvidersCmd.java | 8 ++++---- .../api/command/user/dns/ListDnsRecordsCmd.java | 5 +++-- .../api/command/user/dns/ListDnsServersCmd.java | 5 +++-- .../cloudstack/api/command/user/dns/ListDnsZonesCmd.java | 4 +++- .../api/command/user/dns/RegisterDnsRecordForVmCmd.java | 8 ++++---- .../api/command/user/dns/RemoveDnsRecordForVmCmd.java | 5 ++--- .../api/command/user/dns/UpdateDnsServerCmd.java | 4 +++- .../cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java | 4 +++- .../cloudstack/api/response/DnsProviderResponse.java | 3 --- .../org/apache/cloudstack/dns/DnsProviderManagerImpl.java | 2 ++ 18 files changed, 49 insertions(+), 33 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java index 7f2148d014e5..77525cb89004 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -21,6 +21,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -34,7 +35,8 @@ import org.apache.commons.lang3.BooleanUtils; @APICommand(name = "addDnsServer", description = "Adds a new external DNS server", responseObject = DnsServerResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0") + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class AddDnsServerCmd extends BaseCmd { @Inject diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java index b8cdbd703451..1c3de2916aa4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.dns; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -34,8 +35,8 @@ import com.cloud.exception.ResourceUnavailableException; @APICommand(name = "associateDnsZoneToNetwork", description = "Associates a DNS Zone with a Network for VM auto-registration", - responseObject = DnsZoneNetworkMapResponse.class, requestHasSensitiveInfo = false, - responseHasSensitiveInfo = false, since = "4.23.0") + responseObject = DnsZoneNetworkMapResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class AssociateDnsZoneToNetworkCmd extends BaseCmd { @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java index 077e98c23244..ad4c37122851 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java @@ -19,6 +19,7 @@ import java.util.List; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -36,7 +37,7 @@ @APICommand(name = "createDnsRecord", description = "Creates a DNS record directly on the provider", responseObject = DnsRecordResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0") + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class CreateDnsRecordCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java index 61c7f5b5f44f..4bce7bc8fab7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java @@ -21,6 +21,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -39,8 +40,8 @@ import com.cloud.utils.EnumUtils; @APICommand(name = "createDnsZone", description = "Creates a new DNS Zone on a specific server", - responseObject = DnsZoneResponse.class, requestHasSensitiveInfo = false, - responseHasSensitiveInfo = false, since = "4.23.0") + responseObject = DnsZoneResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { @Inject diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java index 3c9207e7dc74..c0328b36dc05 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.dns; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -33,8 +34,8 @@ import com.cloud.utils.EnumUtils; @APICommand(name = "deleteDnsRecord", description = "Deletes a DNS record from the external provider", - responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, - responseHasSensitiveInfo = false, since = "4.23.0") + responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DeleteDnsRecordCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java index bc6b1f23d053..120866074d84 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.dns; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -31,8 +32,8 @@ import com.cloud.user.Account; @APICommand(name = "deleteDnsServer", description = "Removes a DNS server integration", - responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, - responseHasSensitiveInfo = false, since = "4.23.0") + responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DeleteDnsServerCmd extends BaseAsyncCmd { ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java index b4c5f780a685..b122c1473011 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.dns; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -32,7 +33,7 @@ @APICommand(name = "deleteDnsZone", description = "Removes a DNS Zone from CloudStack and the external provider", responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0") + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DeleteDnsZoneCmd extends BaseAsyncCmd { ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java index c8f8ca7a86b1..0ad41271c760 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.dns; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -34,7 +35,8 @@ import com.cloud.user.Account; @APICommand(name = "disassociateDnsZoneFromNetwork", description = "Removes the association between a DNS Zone and a Network", - responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0") + responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DisassociateDnsZoneFromNetworkCmd extends BaseCmd { @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneNetworkMapResponse.class, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java index 7d808ad2c88f..b847f6518c80 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java @@ -22,6 +22,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.response.DnsProviderResponse; @@ -29,8 +30,8 @@ import org.apache.cloudstack.dns.DnsProviderManager; @APICommand(name = "listDnsProviders", description = "Lists available DNS plugin providers", - responseObject = DnsProviderResponse.class, requestHasSensitiveInfo = false, - responseHasSensitiveInfo = false, since = "4.23.0") + responseObject = DnsProviderResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class ListDnsProvidersCmd extends BaseListCmd { @Inject @@ -42,9 +43,8 @@ public void execute() { ListResponse response = new ListResponse<>(); List responses = new ArrayList<>(); for (String name : providers) { - DnsProviderResponse resp = new DnsProviderResponse(); + DnsProviderResponse resp = new DnsProviderResponse(name); resp.setName(name); - resp.setObjectName("dnsprovider"); responses.add(resp); } response.setResponses(responses); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java index 80d08182f1d0..fcd07e788ce1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.dns; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListCmd; @@ -26,8 +27,8 @@ import org.apache.cloudstack.api.response.ListResponse; @APICommand(name = "listDnsRecords", description = "Lists DNS records from the external provider", - responseObject = DnsRecordResponse.class, requestHasSensitiveInfo = false, - responseHasSensitiveInfo = false, since = "4.23.0") + responseObject = DnsRecordResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class ListDnsRecordsCmd extends BaseListCmd { @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java index fad8cddd32cc..3ee941de1051 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java @@ -19,6 +19,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListAccountResourcesCmd; @@ -28,8 +29,8 @@ import org.apache.cloudstack.dns.DnsProviderManager; @APICommand(name = "listDnsServers", description = "Lists DNS servers owned by the account.", - responseObject = DnsServerResponse.class, requestHasSensitiveInfo = false, - responseHasSensitiveInfo = false, since = "4.23.0") + responseObject = DnsServerResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class ListDnsServersCmd extends BaseListAccountResourcesCmd { @Inject diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java index 6ea329dee8b6..43b2a8758bc6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.api.command.user.dns; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListAccountResourcesCmd; @@ -26,7 +27,8 @@ import org.apache.cloudstack.api.response.ListResponse; @APICommand(name = "listDnsZones", description = "Lists DNS zones.", responseObject = DnsZoneResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0") + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class ListDnsZonesCmd extends BaseListAccountResourcesCmd { ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java index 6e6bd18f4701..1ec858d2510b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java @@ -35,10 +35,10 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; -@APICommand(name = "registerDnsRecordForVm", description = "Automatically registers a DNS record for a VM based on its associated Network and DNS Zone mapping", - responseObject = SuccessResponse.class, - since = "4.23.0", - authorized = {RoleType.Admin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "registerDnsRecordForVm", + description = "Automatically registers a DNS record for a VM based on its associated Network and DNS Zone mapping", + responseObject = SuccessResponse.class, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class RegisterDnsRecordForVmCmd extends BaseCmd { @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java index 01a13168a794..f0ee7d6c61a1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java @@ -36,9 +36,8 @@ import com.cloud.exception.ResourceUnavailableException; @APICommand(name = "removeDnsRecordForVm", description = "Removes the auto-registered DNS record for a VM", - responseObject = SuccessResponse.class, - since = "4.23.0", - authorized = {RoleType.Admin, RoleType.DomainAdmin, RoleType.User}) + responseObject = SuccessResponse.class, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class RemoveDnsRecordForVmCmd extends BaseCmd { @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java index ccd4cdc68777..f2c7ce365ee8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java @@ -19,6 +19,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -35,7 +36,8 @@ import com.cloud.utils.EnumUtils; @APICommand(name = "updateDnsServer", description = "Update DNS server", responseObject = DnsServerResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0") + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class UpdateDnsServerCmd extends BaseCmd { @Inject diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java index 1c191a94509e..69068243d3f3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java @@ -19,6 +19,7 @@ import javax.inject.Inject; +import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -31,7 +32,8 @@ import org.apache.cloudstack.dns.DnsZone; @APICommand(name = "updateDnsZone", description = "Updates a DNS Zone's metadata", responseObject = DnsZoneResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0") + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class UpdateDnsZoneCmd extends BaseCmd { @Inject diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java index f4e892ac85cd..f3a571f40639 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java @@ -29,9 +29,6 @@ public class DnsProviderResponse extends BaseResponse { @Param(description = "The name of the DNS provider (e.g. PowerDNS, Cloudflare)") private String name; - // Constructors - public DnsProviderResponse() {} - public DnsProviderResponse(String name) { this.name = name; setObjectName("dnsprovider"); // Sets the JSON wrapper name 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 b48eece57aaa..d76957fee732 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -59,10 +59,12 @@ import com.cloud.exception.PermissionDeniedException; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.projects.Project; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.utils.Pair; import com.cloud.utils.StringUtils; +import com.cloud.utils.Ternary; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; import com.cloud.utils.db.Filter; From 582b6876d41e929a023551ecaacf911acef8bdde Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Sat, 21 Feb 2026 22:15:08 +0530 Subject: [PATCH 15/23] add ACL annotation, entitytype, minor cleanup --- .../api/command/user/dns/AddDnsServerCmd.java | 15 +++++++-------- .../dns/AssociateDnsZoneToNetworkCmd.java | 10 +++++++--- .../command/user/dns/CreateDnsRecordCmd.java | 12 +++++++++--- .../command/user/dns/CreateDnsZoneCmd.java | 18 +++++++++--------- .../command/user/dns/DeleteDnsRecordCmd.java | 13 ++++++++++--- .../command/user/dns/DeleteDnsServerCmd.java | 13 ++++++++++--- .../command/user/dns/DeleteDnsZoneCmd.java | 16 ++++++++++++---- .../DisassociateDnsZoneFromNetworkCmd.java | 12 +++++++++--- .../command/user/dns/ListDnsProvidersCmd.java | 19 +++++++++---------- .../command/user/dns/ListDnsRecordsCmd.java | 13 ++++++++++--- .../command/user/dns/ListDnsServersCmd.java | 19 ++++++++++--------- .../api/command/user/dns/ListDnsZonesCmd.java | 13 ++++++++++--- .../user/dns/RegisterDnsRecordForVmCmd.java | 8 +++++++- .../user/dns/RemoveDnsRecordForVmCmd.java | 9 +++++++-- .../command/user/dns/UpdateDnsServerCmd.java | 17 +++++++++-------- .../command/user/dns/UpdateDnsZoneCmd.java | 14 ++++++-------- .../dns/DnsProviderManagerImpl.java | 2 -- .../cloudstack/dns/dao/DnsServerDao.java | 1 + 18 files changed, 142 insertions(+), 82 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java index 77525cb89004..03f79a59108b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -19,8 +19,6 @@ import java.util.List; -import javax.inject.Inject; - import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -30,18 +28,19 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.dns.DnsServer; import org.apache.commons.lang3.BooleanUtils; -@APICommand(name = "addDnsServer", description = "Adds a new external DNS server", responseObject = DnsServerResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", +@APICommand(name = "addDnsServer", + description = "Adds a new external DNS server", + responseObject = DnsServerResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class AddDnsServerCmd extends BaseCmd { - @Inject - DnsProviderManager dnsProviderManager; - ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java index 1c3de2916aa4..60f9c70c9c9d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java @@ -34,9 +34,13 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; -@APICommand(name = "associateDnsZoneToNetwork", description = "Associates a DNS Zone with a Network for VM auto-registration", - responseObject = DnsZoneNetworkMapResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "associateDnsZoneToNetwork", + description = "Associates a DNS Zone with a Network for VM auto-registration", + responseObject = DnsZoneNetworkMapResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class AssociateDnsZoneToNetworkCmd extends BaseCmd { @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java index ad4c37122851..ab00c8d8c7eb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java @@ -20,6 +20,7 @@ import java.util.List; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -35,11 +36,16 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.utils.EnumUtils; -@APICommand(name = "createDnsRecord", description = "Creates a DNS record directly on the provider", - responseObject = DnsRecordResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "createDnsRecord", + description = "Creates a DNS record directly on the provider", + responseObject = DnsRecordResponse.class, + entityType = {DnsRecord.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class CreateDnsRecordCmd extends BaseAsyncCmd { + @ACL @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, description = "ID of the DNS zone") private Long dnsZoneId; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java index 4bce7bc8fab7..b69752e67b26 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java @@ -19,9 +19,8 @@ import java.util.Arrays; -import javax.inject.Inject; - import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -31,7 +30,6 @@ import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.dns.DnsZone; import org.apache.commons.lang3.StringUtils; @@ -39,14 +37,15 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.utils.EnumUtils; -@APICommand(name = "createDnsZone", description = "Creates a new DNS Zone on a specific server", - responseObject = DnsZoneResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "createDnsZone", + description = "Creates a new DNS Zone on a specific server", + responseObject = DnsZoneResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { - @Inject - DnsProviderManager dnsProviderManager; - ///////////////////////////////////////////////////// //////////////// API Parameters ///////////////////// ///////////////////////////////////////////////////// @@ -55,6 +54,7 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { description = "The name of the DNS zone (e.g. example.com)") private String name; + @ACL @Parameter(name = ApiConstants.DNS_SERVER_ID, type = CommandType.UUID, entityType = DnsServerResponse.class, required = true, description = "The ID of the DNS server to host this zone") private Long dnsServerId; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java index c0328b36dc05..1d7914c346e9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java @@ -18,6 +18,8 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -33,11 +35,16 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.utils.EnumUtils; -@APICommand(name = "deleteDnsRecord", description = "Deletes a DNS record from the external provider", - responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "deleteDnsRecord", + description = "Deletes a DNS record from the external provider", + responseObject = SuccessResponse.class, + entityType = {DnsRecord.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DeleteDnsRecordCmd extends BaseAsyncCmd { + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, description = "The ID of the DNS zone") private Long dnsZoneId; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java index 120866074d84..6cc86312f246 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java @@ -18,6 +18,8 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -31,15 +33,20 @@ import com.cloud.event.EventTypes; import com.cloud.user.Account; -@APICommand(name = "deleteDnsServer", description = "Removes a DNS server integration", - responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "deleteDnsServer", + description = "Removes a DNS server integration", + responseObject = SuccessResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DeleteDnsServerCmd extends BaseAsyncCmd { ///////////////////////////////////////////////////// //////////////// API Parameters ///////////////////// ///////////////////////////////////////////////////// + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, required = true, description = "the ID of the DNS server") private Long id; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java index b122c1473011..88b3713fe2e0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java @@ -18,6 +18,8 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -31,16 +33,22 @@ import com.cloud.event.EventTypes; import com.cloud.user.Account; -@APICommand(name = "deleteDnsZone", description = "Removes a DNS Zone from CloudStack and the external provider", - responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "deleteDnsZone", + description = "Removes a DNS Zone from CloudStack and the external provider", + responseObject = SuccessResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DeleteDnsZoneCmd extends BaseAsyncCmd { ///////////////////////////////////////////////////// //////////////// API Parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, description = "The ID of the DNS zone") + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, + description = "The ID of the DNS zone") private Long id; ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java index 0ad41271c760..bd31737d30e9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java @@ -18,6 +18,8 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -34,11 +36,15 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.user.Account; -@APICommand(name = "disassociateDnsZoneFromNetwork", description = "Removes the association between a DNS Zone and a Network", - responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "disassociateDnsZoneFromNetwork", + description = "Removes the association between a DNS Zone and a Network", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DisassociateDnsZoneFromNetworkCmd extends BaseCmd { + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneNetworkMapResponse.class, required = true, description = "The ID of the DNS zone to network mapping") private Long id; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java index b847f6518c80..800f030754b9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java @@ -20,26 +20,25 @@ import java.util.ArrayList; import java.util.List; -import javax.inject.Inject; - import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.response.DnsProviderResponse; import org.apache.cloudstack.api.response.ListResponse; -import org.apache.cloudstack.dns.DnsProviderManager; +import org.apache.cloudstack.dns.DnsProvider; -@APICommand(name = "listDnsProviders", description = "Lists available DNS plugin providers", - responseObject = DnsProviderResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "listDnsProviders", + description = "Lists available DNS plugin providers", + responseObject = DnsProviderResponse.class, + entityType = {DnsProvider.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class ListDnsProvidersCmd extends BaseListCmd { - @Inject - DnsProviderManager dnsManager; - @Override public void execute() { - List providers = dnsManager.listProviderNames(); + List providers = dnsProviderManager.listProviderNames(); ListResponse response = new ListResponse<>(); List responses = new ArrayList<>(); for (String name : providers) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java index fcd07e788ce1..3bfa4af18dbe 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListCmd; @@ -25,12 +26,18 @@ import org.apache.cloudstack.api.response.DnsRecordResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsRecord; -@APICommand(name = "listDnsRecords", description = "Lists DNS records from the external provider", - responseObject = DnsRecordResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "listDnsRecords", + description = "Lists DNS records from the external provider", + responseObject = DnsRecordResponse.class, + entityType = {DnsRecord.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class ListDnsRecordsCmd extends BaseListCmd { + @ACL @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, description = "ID of the DNS zone to list records from") private Long dnsZoneId; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java index 3ee941de1051..fa301bf2851b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java @@ -17,29 +17,30 @@ package org.apache.cloudstack.api.command.user.dns; -import javax.inject.Inject; - import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListAccountResourcesCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.ListResponse; -import org.apache.cloudstack.dns.DnsProviderManager; +import org.apache.cloudstack.dns.DnsServer; -@APICommand(name = "listDnsServers", description = "Lists DNS servers owned by the account.", - responseObject = DnsServerResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, - since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +@APICommand(name = "listDnsServers", + description = "Lists DNS servers owned by the account.", + responseObject = DnsServerResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class ListDnsServersCmd extends BaseListAccountResourcesCmd { - @Inject - DnsProviderManager dnsProviderManager; - ///////////////////////////////////////////////////// //////////////// API Parameters ///////////////////// ///////////////////////////////////////////////////// + @ACL @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, description = "the ID of the DNS server") private Long id; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java index 43b2a8758bc6..f848c3525d21 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListAccountResourcesCmd; @@ -25,20 +26,26 @@ import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsZone; -@APICommand(name = "listDnsZones", description = "Lists DNS zones.", responseObject = DnsZoneResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", +@APICommand(name = "listDnsZones", + description = "Lists DNS zones.", responseObject = DnsZoneResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class ListDnsZonesCmd extends BaseListAccountResourcesCmd { ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - /// + + @ACL @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, description = "List DNS zone by ID") private Long id; + @ACL @Parameter(name = "dnsserverid", type = CommandType.UUID, entityType = DnsServerResponse.class, description = "List DNS zones belonging to a specific DNS server") private Long dnsServerId; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java index 1ec858d2510b..935c78e85fba 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -28,6 +29,7 @@ import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsRecord; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientCapacityException; @@ -37,14 +39,18 @@ @APICommand(name = "registerDnsRecordForVm", description = "Automatically registers a DNS record for a VM based on its associated Network and DNS Zone mapping", - responseObject = SuccessResponse.class, since = "4.23.0", + responseObject = SuccessResponse.class, + entityType = {DnsRecord.class}, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class RegisterDnsRecordForVmCmd extends BaseCmd { + @ACL @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, required = true, description = "The ID of the Virtual Machine") private Long vmId; + @ACL @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, description = "The ID of the network. If not specified, the VM's default NIC network is used.") private Long networkId; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java index f0ee7d6c61a1..0d1adf7cfb03 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -35,15 +36,19 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; -@APICommand(name = "removeDnsRecordForVm", description = "Removes the auto-registered DNS record for a VM", - responseObject = SuccessResponse.class, since = "4.23.0", +@APICommand(name = "removeDnsRecordForVm", + description = "Removes the auto-registered DNS record for a VM", + responseObject = SuccessResponse.class, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class RemoveDnsRecordForVmCmd extends BaseCmd { + @ACL @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, required = true, description = "The ID of the Virtual Machine") private Long vmId; + @ACL @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, description = "The ID of the network. If not specified, the VM's default NIC network is used.") private Long networkId; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java index f2c7ce365ee8..4077f7f1b91f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java @@ -17,9 +17,9 @@ package org.apache.cloudstack.api.command.user.dns; -import javax.inject.Inject; - import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -28,25 +28,26 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.dns.DnsServer; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import com.cloud.utils.EnumUtils; -@APICommand(name = "updateDnsServer", description = "Update DNS server", responseObject = DnsServerResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", +@APICommand(name = "updateDnsServer", + description = "Update DNS server", + responseObject = DnsServerResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class UpdateDnsServerCmd extends BaseCmd { - @Inject - DnsProviderManager dnsProviderManager; - ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, required = true, description = "The ID of the DNS server to update") private Long id; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java index 69068243d3f3..d4ebe71396c5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java @@ -17,8 +17,6 @@ package org.apache.cloudstack.api.command.user.dns; -import javax.inject.Inject; - import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; @@ -28,17 +26,17 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.dns.DnsZone; -@APICommand(name = "updateDnsZone", description = "Updates a DNS Zone's metadata", responseObject = DnsZoneResponse.class, - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", +@APICommand(name = "updateDnsZone", + description = "Updates a DNS Zone's metadata", + responseObject = DnsZoneResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class UpdateDnsZoneCmd extends BaseCmd { - @Inject - DnsProviderManager dnsProviderManager; - ///////////////////////////////////////////////////// //////////////// API Parameters ///////////////////// ///////////////////////////////////////////////////// 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 d76957fee732..b48eece57aaa 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -59,12 +59,10 @@ import com.cloud.exception.PermissionDeniedException; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; -import com.cloud.projects.Project; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.utils.Pair; import com.cloud.utils.StringUtils; -import com.cloud.utils.Ternary; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; import com.cloud.utils.db.Filter; diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java index 5cbf5e3256d9..d68e456a6dd3 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java @@ -28,5 +28,6 @@ public interface DnsServerDao extends GenericDao { DnsServer findByUrlAndAccount(String url, long accountId); + Pair, Integer> searchDnsServers(Long id, String keyword, String provider, Long accountId, Filter filter); } From add77636d9866f4364401104e27507a599e7728d Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 25 Feb 2026 11:38:38 +0530 Subject: [PATCH 16/23] acl related changes, fixes, mistakes --- .../command/user/dns/DeleteDnsServerCmd.java | 5 +- .../command/user/dns/ListDnsRecordsCmd.java | 2 - .../command/user/dns/ListDnsServersCmd.java | 2 - .../api/command/user/dns/ListDnsZonesCmd.java | 3 - .../command/user/dns/UpdateDnsServerCmd.java | 9 +- .../cloudstack/dns/DnsProviderManager.java | 6 +- .../org/apache/cloudstack/dns/DnsServer.java | 2 +- ...UtilTest.java => DnsProviderUtilTest.java} | 18 +- .../java/com/cloud/acl/DomainChecker.java | 12 +- .../network/firewall/FirewallManagerImpl.java | 2 +- .../com/cloud/user/AccountManagerImpl.java | 4 +- .../dns/DnsProviderManagerImpl.java | 174 +++++++++++++----- .../{DnsUtil.java => DnsProviderUtil.java} | 3 +- .../cloudstack/dns/dao/DnsServerDao.java | 2 + .../cloudstack/dns/dao/DnsServerDaoImpl.java | 16 ++ .../apache/cloudstack/dns/dao/DnsZoneDao.java | 4 +- .../cloudstack/dns/dao/DnsZoneDaoImpl.java | 29 ++- .../apache/cloudstack/dns/vo/DnsServerVO.java | 12 +- 18 files changed, 217 insertions(+), 88 deletions(-) rename plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/{DnsUtilTest.java => DnsProviderUtilTest.java} (89%) rename server/src/main/java/org/apache/cloudstack/dns/{DnsUtil.java => DnsProviderUtil.java} (97%) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java index 6cc86312f246..2fb8a9903e4c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java @@ -80,13 +80,10 @@ public void execute() { @Override public long getEntityOwnerId() { - // Look up the server to find its owner. - // This allows the Framework to check: Is Caller == Owner? - DnsServer server = dnsProviderManager.getDnsServer(id); + DnsServer server = _entityMgr.findById(DnsServer.class, id); if (server != null) { return server.getAccountId(); } - // If server not found, return System to fail safely (or let manager handle 404) return Account.ACCOUNT_ID_SYSTEM; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java index 3bfa4af18dbe..e1c6b6b42e73 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java @@ -18,7 +18,6 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; -import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListCmd; @@ -37,7 +36,6 @@ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class ListDnsRecordsCmd extends BaseListCmd { - @ACL @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, description = "ID of the DNS zone to list records from") private Long dnsZoneId; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java index fa301bf2851b..39cb24a3ec33 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java @@ -18,7 +18,6 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; -import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListAccountResourcesCmd; @@ -40,7 +39,6 @@ public class ListDnsServersCmd extends BaseListAccountResourcesCmd { //////////////// API Parameters ///////////////////// ///////////////////////////////////////////////////// - @ACL @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, description = "the ID of the DNS server") private Long id; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java index f848c3525d21..7d6f17a28f0f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java @@ -18,7 +18,6 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; -import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListAccountResourcesCmd; @@ -40,12 +39,10 @@ public class ListDnsZonesCmd extends BaseListAccountResourcesCmd { //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @ACL @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, description = "List DNS zone by ID") private Long id; - @ACL @Parameter(name = "dnsserverid", type = CommandType.UUID, entityType = DnsServerResponse.class, description = "List DNS zones belonging to a specific DNS server") private Long dnsServerId; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java index 4077f7f1b91f..52ba0497e743 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java @@ -27,11 +27,11 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DnsServerResponse; -import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.dns.DnsServer; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; +import com.cloud.user.Account; import com.cloud.utils.EnumUtils; @APICommand(name = "updateDnsServer", @@ -99,7 +99,12 @@ public String getPublicDomainSuffix() { @Override public long getEntityOwnerId() { - return CallContext.current().getCallingAccount().getId(); + DnsServer server = _entityMgr.findById(DnsServer.class, id); + if (server != null) { + return server.getAccountId(); + } + // If server not found, return System to fail safely (or let manager handle 404) + return Account.ACCOUNT_ID_SYSTEM; } @Override 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 2adb1d8f3c5b..72a793328af0 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -39,6 +39,7 @@ import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; +import com.cloud.user.Account; import com.cloud.utils.component.Manager; import com.cloud.utils.component.PluggableService; @@ -50,8 +51,6 @@ public interface DnsProviderManager extends Manager, PluggableService { boolean deleteDnsServer(DeleteDnsServerCmd cmd); DnsServerResponse createDnsServerResponse(DnsServer server); - DnsServer getDnsServer(Long id); - // Allocates the DB row (State: Inactive) DnsZone allocateDnsZone(CreateDnsZoneCmd cmd); // Calls the Plugin (State: Inactive -> Active) @@ -78,4 +77,7 @@ public interface DnsProviderManager extends Manager, PluggableService { boolean registerDnsRecordForVm(RegisterDnsRecordForVmCmd cmd); boolean removeDnsRecordForVm(RemoveDnsRecordForVmCmd cmd); + + void checkDnsServerPermissions(Account caller, DnsServer server); + void checkDnsZonePermission(Account caller, DnsZone zone); } diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java index 49f7ddea5b4a..0261902ef98c 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java @@ -41,7 +41,7 @@ enum State { long getAccountId(); - boolean isPublic(); + boolean isPublicServer(); Date getCreated(); diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsUtilTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java similarity index 89% rename from plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsUtilTest.java rename to plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java index bc155e8d982a..25d8f1583726 100644 --- a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsUtilTest.java +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java @@ -17,8 +17,8 @@ package org.apache.cloudstack.dns; -import static org.apache.cloudstack.dns.DnsUtil.appendPublicSuffixToZone; -import static org.apache.cloudstack.dns.DnsUtil.normalizeDomain; +import static org.apache.cloudstack.dns.DnsProviderUtil.appendPublicSuffixToZone; +import static org.apache.cloudstack.dns.DnsProviderUtil.normalizeDomain; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -31,16 +31,16 @@ import org.junit.runners.Parameterized; @RunWith(Parameterized.class) -public class DnsUtilTest { +public class DnsProviderUtilTest { private final String userZoneName; private final String publicSuffix; private final String expectedResult; private final boolean expectException; - public DnsUtilTest(String userZoneName, - String publicSuffix, - String expectedResult, - boolean expectException) { + public DnsProviderUtilTest(String userZoneName, + String publicSuffix, + String expectedResult, + boolean expectException) { this.userZoneName = userZoneName; this.publicSuffix = publicSuffix; this.expectedResult = expectedResult; @@ -96,8 +96,6 @@ public void testAppendPublicSuffix() { } String executeAppendSuffixTest(String zoneName, String domainSuffix) { - return appendPublicSuffixToZone( - normalizeDomain(zoneName), - normalizeDomain(domainSuffix)); + return appendPublicSuffixToZone(normalizeDomain(zoneName), domainSuffix); } } diff --git a/server/src/main/java/com/cloud/acl/DomainChecker.java b/server/src/main/java/com/cloud/acl/DomainChecker.java index 0500960abb13..ba4f5e530311 100644 --- a/server/src/main/java/com/cloud/acl/DomainChecker.java +++ b/server/src/main/java/com/cloud/acl/DomainChecker.java @@ -28,6 +28,9 @@ import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderManager; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.DnsZone; import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; import org.springframework.stereotype.Component; @@ -101,6 +104,8 @@ public class DomainChecker extends AdapterBase implements SecurityChecker { private ProjectDao projectDao; @Inject private AccountService accountService; + @Inject + private DnsProviderManager dnsProviderManager; protected DomainChecker() { super(); @@ -216,7 +221,12 @@ public boolean checkAccess(Account caller, ControlledEntity entity, AccessType a _networkMgr.checkRouterPermissions(caller, (VirtualRouter)entity); } else if (entity instanceof AffinityGroup) { return false; - } else { + } else if (entity instanceof DnsServer) { + dnsProviderManager.checkDnsServerPermissions(caller, (DnsServer) entity); + } else if (entity instanceof DnsZone) { + dnsProviderManager.checkDnsZonePermission(caller, (DnsZone) entity); + } + else { validateCallerHasAccessToEntityOwner(caller, entity, accessType); } return true; diff --git a/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java b/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java index 00863c28dd22..32a7a972129c 100644 --- a/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java +++ b/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java @@ -311,7 +311,7 @@ public Pair, Integer> listFirewallRules(IListFirewa sb.and("id", sb.entity().getId(), Op.EQ); sb.and("trafficType", sb.entity().getTrafficType(), Op.EQ); - sb.and("networkId", sb.entity().getNetworkId(), Op.EQ); + sb.and("networkId", sb.entity().getNetworkId(), Op.EQ); sb.and("ip", sb.entity().getSourceIpAddressId(), Op.EQ); sb.and("purpose", sb.entity().getPurpose(), Op.EQ); sb.and("display", sb.entity().isDisplay(), Op.EQ); diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 53b88690654e..e7395b69131c 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -70,6 +70,7 @@ import org.apache.cloudstack.backup.BackupOffering; import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -744,7 +745,8 @@ public void checkAccess(Account caller, AccessType accessType, boolean sameOwner } if (entity.getAccountId() != -1 && domainId != -1 && !(entity instanceof VirtualMachineTemplate) && !(entity instanceof Network && accessType != null && (accessType == AccessType.UseEntry || accessType == AccessType.OperateEntry)) - && !(entity instanceof AffinityGroup) && !(entity instanceof VirtualRouter)) { + && !(entity instanceof AffinityGroup) && !(entity instanceof VirtualRouter) + && !(entity instanceof DnsServer)) { List toBeChecked = domains.get(entity.getDomainId()); // for templates, we don't have to do cross domains check if (toBeChecked == 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 b48eece57aaa..b79a59342dfb 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -55,17 +55,23 @@ import org.apache.cloudstack.dns.vo.DnsZoneVO; import org.springframework.stereotype.Component; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.projects.Project; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.utils.Pair; import com.cloud.utils.StringUtils; +import com.cloud.utils.Ternary; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.NicVO; import com.cloud.vm.UserVmVO; @@ -89,6 +95,8 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa UserVmDao userVmDao; @Inject NicDao nicDao; + @Inject + DomainDao domainDao; private DnsProvider getProvider(DnsProviderType type) { if (type == null) { @@ -120,7 +128,7 @@ public DnsServer addDnsServer(AddDnsServerCmd cmd) { } if (StringUtils.isNotBlank(publicDomainSuffix)) { - publicDomainSuffix = DnsUtil.normalizeDomain(publicDomainSuffix); + publicDomainSuffix = DnsProviderUtil.normalizeDomain(publicDomainSuffix); } DnsProviderType type = DnsProviderType.fromString(cmd.getProvider()); @@ -142,26 +150,7 @@ public DnsServer addDnsServer(AddDnsServerCmd cmd) { @Override public ListResponse listDnsServers(ListDnsServersCmd cmd) { - Account caller = CallContext.current().getCallingAccount(); - Long accountIdFilter = null; - if (accountMgr.isRootAdmin(caller.getId())) { - // Root Admin: Can see all, unless they specifically ask for an account - if (cmd.getAccountName() != null) { - Account target = accountMgr.getActiveAccountByName(cmd.getAccountName(), cmd.getDomainId()); - if (target == null) { - return new ListResponse<>(); // Account not found - } - accountIdFilter = target.getId(); - } - } else { - // Regular User / Domain Admin: STRICTLY restricted to their own account - accountIdFilter = caller.getId(); - } - - Filter searchFilter = new Filter(DnsServerVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); - Pair, Integer> result = dnsServerDao.searchDnsServers(cmd.getId(), cmd.getKeyword(), - cmd.getProvider(), accountIdFilter, searchFilter); - + Pair, Integer> result = searchForDnsServerInternal(cmd); ListResponse response = new ListResponse<>(); List serverResponses = new ArrayList<>(); for (DnsServerVO server : result.first()) { @@ -171,6 +160,76 @@ public ListResponse listDnsServers(ListDnsServersCmd cmd) { return response; } + private Pair, Integer> searchForDnsServerInternal(ListDnsServersCmd cmd) { + Long dnsServerId = cmd.getId(); + Account caller = CallContext.current().getCallingAccount(); + Long domainId = cmd.getDomainId(); + boolean isRecursive = cmd.isRecursive(); + + // Step 1: Build ACL search parameters based on caller permissions + List permittedAccountIds = new ArrayList<>(); + Ternary domainIdRecursiveListProject = new Ternary<>( + domainId, isRecursive, null); + accountMgr.buildACLSearchParameters(caller, dnsServerId, cmd.getAccountName(), null, permittedAccountIds, + domainIdRecursiveListProject, cmd.listAll(), false); + + domainId = domainIdRecursiveListProject.first(); + isRecursive = domainIdRecursiveListProject.second(); + Project.ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); + Filter searchFilter = new Filter(DnsServerVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + + // Step 2: Search for caller's own DNS servers using standard ACL pattern + SearchBuilder sb = dnsServerDao.createSearchBuilder(); + accountMgr.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); + sb.and("state", sb.entity().getState(), SearchCriteria.Op.EQ); + sb.done(); + + SearchCriteria sc = sb.create(); + accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); + sc.setParameters("state", DnsServer.State.Enabled); + + Pair, Integer> ownServersPair = dnsServerDao.searchAndCount(sc, searchFilter); + List dnsServers = new ArrayList<>(ownServersPair.first()); + int count = ownServersPair.second(); + + // Step 3: Search for public DNS servers from caller's domain and children + // domains + Long callerDomainId = caller.getDomainId(); + DomainVO callerDomain = domainDao.findById(callerDomainId); + if (callerDomain != null) { + List domainIds = new ArrayList<>(); + domainIds.add(callerDomainId); + List childDomains = domainDao.findAllChildren(callerDomain.getPath(), callerDomainId); + for (DomainVO childDomain : childDomains) { + domainIds.add(childDomain.getId()); + } + + SearchBuilder publicSb = dnsServerDao.createSearchBuilder(); + publicSb.and("publicDns", publicSb.entity().isPublicServer(), SearchCriteria.Op.EQ); + publicSb.and("publicDomainId", publicSb.entity().getDomainId(), SearchCriteria.Op.IN); + publicSb.and("publicState", publicSb.entity().getState(), SearchCriteria.Op.EQ); + publicSb.done(); + + SearchCriteria publicSc = publicSb.create(); + publicSc.setParameters("publicDns", 1); + publicSc.setParameters("publicDomainId", domainIds.toArray()); + publicSc.setParameters("publicState", DnsServer.State.Enabled); + + List publicServers = dnsServerDao.search(publicSc, null); + + // Deduplicate: add only public servers not already in the own servers list + List ownServerIds = dnsServers.stream().map(DnsServerVO::getId).collect(Collectors.toList()); + for (DnsServerVO publicServer : publicServers) { + if (!ownServerIds.contains(publicServer.getId())) { + dnsServers.add(publicServer); + count++; + } + } + } + + return new Pair<>(dnsServers, count); + } + @Override public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { Long dnsServerId = cmd.getId(); @@ -214,7 +273,7 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { } if (cmd.getPublicDomainSuffix() != null) { - dnsServer.setPublicDomainSuffix(DnsUtil.normalizeDomain(cmd.getPublicDomainSuffix())); + dnsServer.setPublicDomainSuffix(DnsProviderUtil.normalizeDomain(cmd.getPublicDomainSuffix())); } if (cmd.getNameServers() != null) { @@ -235,7 +294,6 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { } boolean updateStatus = dnsServerDao.update(dnsServerId, dnsServer); - if (updateStatus) { return dnsServerDao.findById(dnsServerId); } else { @@ -263,18 +321,13 @@ public DnsServerResponse createDnsServerResponse(DnsServer server) { response.setUrl(server.getUrl()); response.setPort(server.getPort()); response.setProvider(server.getProviderType()); - response.setPublic(server.isPublic()); + response.setPublic(server.isPublicServer()); response.setNameServers(server.getNameServers()); response.setPublicDomainSuffix(server.getPublicDomainSuffix()); response.setObjectName("dnsserver"); return response; } - @Override - public DnsServer getDnsServer(Long id) { - return null; - } - @Override public boolean deleteDnsZone(Long zoneId) { DnsZoneVO zone = dnsZoneDao.findById(zoneId); @@ -335,14 +388,8 @@ public DnsZone updateDnsZone(UpdateDnsZoneCmd cmd) { @Override public ListResponse listDnsZones(ListDnsZonesCmd cmd) { - Account caller = CallContext.current().getCallingAccount(); - Filter searchFilter = new Filter(DnsZoneVO.class, ApiConstants.ID, true, cmd.getStartIndex(), cmd.getPageSizeVal()); - Long accountIdFilter = caller.getAccountId(); - String keyword = cmd.getKeyword(); - if (cmd.getName() != null) { - keyword = cmd.getName(); - } - Pair, Integer> result = dnsZoneDao.searchZones(cmd.getId(), cmd.getDnsServerId(), keyword, accountIdFilter, searchFilter); + Pair, Integer> result = searchForDnsZonesInternal(cmd); + List zoneResponses = new ArrayList<>(); for (DnsZoneVO zone : result.first()) { zoneResponses.add(createDnsZoneResponse(zone)); @@ -352,6 +399,27 @@ public ListResponse listDnsZones(ListDnsZonesCmd cmd) { return response; } + private Pair, Integer> searchForDnsZonesInternal(ListDnsZonesCmd cmd) { + if (cmd.getId() != null) { + DnsZone dnsZone = dnsZoneDao.findById(cmd.getId()); + if (dnsZone == null) { + throw new InvalidParameterValueException("DNS zone not found for the given ID"); + } + } + Account caller = CallContext.current().getCallingAccount(); + if (cmd.getDnsServerId() != null) { + DnsServer dnsServer = dnsServerDao.findById(cmd.getDnsServerId()); + accountMgr.checkAccess(caller, null, false, dnsServer); + } + List ownDnsServerIds = dnsServerDao.listDnsServerIdsByAccountId(caller.getAccountId()); + String keyword = cmd.getKeyword(); + if (cmd.getName() != null) { + keyword = cmd.getName(); + } + Filter searchFilter = new Filter(DnsZoneVO.class, ApiConstants.ID, true, cmd.getStartIndex(), cmd.getPageSizeVal()); + return dnsZoneDao.searchZones(cmd.getId(), caller.getAccountId(), ownDnsServerIds, cmd.getDnsServerId(), keyword, searchFilter); + } + @Override public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { String recordName = StringUtils.trimToEmpty(cmd.getName()).toLowerCase(); @@ -368,7 +436,7 @@ public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { try { DnsRecord.RecordType type = cmd.getType(); List normalizedContents = cmd.getContents().stream() - .map(value -> DnsUtil.normalizeDnsRecordValue(value, type)).collect(Collectors.toList()); + .map(value -> DnsProviderUtil.normalizeDnsRecordValue(value, type)).collect(Collectors.toList()); DnsRecord record = new DnsRecord(recordName, type, normalizedContents, cmd.getTtl()); DnsProvider provider = getProvider(server.getProviderType()); String normalizedRecordName = provider.addRecord(server, zone, record); @@ -452,19 +520,18 @@ public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) { throw new InvalidParameterValueException("DNS zone name cannot be empty"); } - String dnsZoneName = DnsUtil.normalizeDomain(cmd.getName()); + String dnsZoneName = DnsProviderUtil.normalizeDomain(cmd.getName()); DnsServerVO server = dnsServerDao.findById(cmd.getDnsServerId()); if (server == null) { throw new InvalidParameterValueException(String.format("DNS server not found for the given ID: %s", cmd.getDnsServerId())); } - Account caller = CallContext.current().getCallingAccount(); boolean isOwner = (server.getAccountId() == caller.getId()); if (!isOwner) { - if (!server.isPublic()) { + if (!server.isPublicServer()) { throw new PermissionDeniedException("You do not have permission to use this DNS server."); } - dnsZoneName = DnsUtil.appendPublicSuffixToZone(dnsZoneName, DnsUtil.normalizeDomain(server.getPublicDomainSuffix())); + dnsZoneName = DnsProviderUtil.appendPublicSuffixToZone(dnsZoneName, server.getPublicDomainSuffix()); } DnsZone.ZoneType type = cmd.getType(); DnsZoneVO existing = dnsZoneDao.findByNameServerAndType(dnsZoneName, server.getId(), type); @@ -577,6 +644,27 @@ public boolean removeDnsRecordForVm(RemoveDnsRecordForVmCmd cmd) { return processDnsRecordForInstance(cmd.getVmId(), cmd.getNetworkId(), false); } + @Override + public void checkDnsServerPermissions(Account caller, DnsServer server) { + if (caller.getId() == server.getAccountId()) { + return; + } + if (!server.isPublicServer()) { + throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + server.getName()); + } + Account owner = accountMgr.getAccount(server.getAccountId()); + if (!domainDao.isChildDomain(owner.getDomainId(), caller.getDomainId())) { + throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + server.getName()); + } + } + + @Override + public void checkDnsZonePermission(Account caller, DnsZone zone) { + if (caller.getId() != zone.getAccountId()) { + throw new PermissionDeniedException(caller + "is not allowed to access the DNS Zone " + zone.getName()); + } + } + /** * Helper method to handle both Register and Remove logic for Instance */ @@ -623,7 +711,7 @@ private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boo // Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain") String recordName = String.valueOf(instance.getInstanceName()); - if (StringUtils.isNotBlank(map.getSubDomain() )) { + if (StringUtils.isNotBlank(map.getSubDomain())) { recordName = recordName + "." + map.getSubDomain(); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsUtil.java b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderUtil.java similarity index 97% rename from server/src/main/java/org/apache/cloudstack/dns/DnsUtil.java rename to server/src/main/java/org/apache/cloudstack/dns/DnsProviderUtil.java index dc312f55287d..95f04fe09408 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsUtil.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderUtil.java @@ -21,13 +21,14 @@ import com.cloud.utils.StringUtils; -public class DnsUtil { +public class DnsProviderUtil { static DomainValidator validator = DomainValidator.getInstance(true); public static String appendPublicSuffixToZone(String zoneName, String suffixDomain) { if (StringUtils.isBlank(suffixDomain)) { return zoneName; } + suffixDomain = DnsProviderUtil.normalizeDomain(suffixDomain); // Already suffixed → return as-is if (zoneName.toLowerCase().endsWith("." + suffixDomain.toLowerCase())) { return zoneName; diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java index d68e456a6dd3..c9cd68c05130 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java @@ -29,5 +29,7 @@ public interface DnsServerDao extends GenericDao { DnsServer findByUrlAndAccount(String url, long accountId); + List listDnsServerIdsByAccountId(Long accountId); + Pair, Integer> searchDnsServers(Long id, String keyword, String provider, Long accountId, Filter filter); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java index 7e4559c51fd2..8a7311e2f1a9 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java @@ -27,6 +27,7 @@ import com.cloud.utils.Pair; import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.GenericSearchBuilder; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @@ -34,6 +35,7 @@ public class DnsServerDaoImpl extends GenericDaoBase implements DnsServerDao { SearchBuilder AllFieldsSearch; SearchBuilder AccountUrlSearch; + GenericSearchBuilder DnsServerIdsByAccountSearch; public DnsServerDaoImpl() { @@ -51,6 +53,11 @@ public DnsServerDaoImpl() { AllFieldsSearch.and(ApiConstants.ACCOUNT_ID, AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ); AllFieldsSearch.done(); + DnsServerIdsByAccountSearch = createSearchBuilder(Long.class); + DnsServerIdsByAccountSearch.selectFields(DnsServerIdsByAccountSearch.entity().getId()); + DnsServerIdsByAccountSearch.and(ApiConstants.ACCOUNT_ID, DnsServerIdsByAccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + DnsServerIdsByAccountSearch.done(); + } @Override @@ -61,6 +68,15 @@ public DnsServer findByUrlAndAccount(String url, long accountId) { return findOneBy(sc); } + @Override + public List listDnsServerIdsByAccountId(Long accountId) { + SearchCriteria sc = DnsServerIdsByAccountSearch.create(); + if (accountId != null) { + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + } + return customSearch(sc, null); + } + @Override public Pair, Integer> searchDnsServers(Long id, String keyword, String provider, Long accountId, Filter filter) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java index 1d58b5275036..05fb799a663a 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java @@ -29,5 +29,7 @@ public interface DnsZoneDao extends GenericDao { List listByAccount(long accountId); DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type); - Pair, Integer> searchZones(Long id, Long dnsServerId, String keyword, Long accountId, Filter filter); + + Pair, Integer> searchZones(Long id, Long accountId, List ownDnsServerIds, Long targetDnsServerId, + String keyword, Filter filter); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java index 2658b8e2987a..9ad1217cb711 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java @@ -22,6 +22,7 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.dns.DnsZone; import org.apache.cloudstack.dns.vo.DnsZoneVO; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.utils.Pair; @@ -32,7 +33,6 @@ @Component public class DnsZoneDaoImpl extends GenericDaoBase implements DnsZoneDao { - static final String DNS_SERVER_ID = "dnsServerId"; SearchBuilder AccountSearch; SearchBuilder NameServerTypeSearch; SearchBuilder AllFieldsSearch; @@ -42,19 +42,24 @@ public DnsZoneDaoImpl() { AccountSearch = createSearchBuilder(); AccountSearch.and(ApiConstants.ACCOUNT_ID, AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountSearch.and(ApiConstants.STATE, AccountSearch.entity().getState(), SearchCriteria.Op.EQ); AccountSearch.done(); NameServerTypeSearch = createSearchBuilder(); NameServerTypeSearch.and(ApiConstants.NAME, NameServerTypeSearch.entity().getName(), SearchCriteria.Op.EQ); - NameServerTypeSearch.and(DNS_SERVER_ID, NameServerTypeSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); + NameServerTypeSearch.and(ApiConstants.DNS_SERVER_ID, NameServerTypeSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); NameServerTypeSearch.and(ApiConstants.TYPE, NameServerTypeSearch.entity().getType(), SearchCriteria.Op.EQ); + NameServerTypeSearch.and(ApiConstants.STATE, NameServerTypeSearch.entity().getState(), SearchCriteria.Op.EQ); NameServerTypeSearch.done(); AllFieldsSearch = createSearchBuilder(); + AllFieldsSearch.and().op(ApiConstants.DNS_SERVER_ID, AllFieldsSearch.entity().getDnsServerId(), SearchCriteria.Op.IN); + AllFieldsSearch.or(ApiConstants.ACCOUNT_ID, AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AllFieldsSearch.cp(); + AllFieldsSearch.and(ApiConstants.STATE, AllFieldsSearch.entity().getState(), SearchCriteria.Op.EQ); AllFieldsSearch.and(ApiConstants.ID, AllFieldsSearch.entity().getId(), SearchCriteria.Op.EQ); - AllFieldsSearch.and(DNS_SERVER_ID, AllFieldsSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); AllFieldsSearch.and(ApiConstants.NAME, AllFieldsSearch.entity().getName(), SearchCriteria.Op.LIKE); - AllFieldsSearch.and(ApiConstants.ACCOUNT_ID, AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and(ApiConstants.TARGET_ID, AllFieldsSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); AllFieldsSearch.done(); } @@ -62,6 +67,7 @@ public DnsZoneDaoImpl() { public List listByAccount(long accountId) { SearchCriteria sc = AccountSearch.create(); sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + sc.setParameters(ApiConstants.STATE, DnsZone.State.Active); return listBy(sc); } @@ -69,19 +75,22 @@ public List listByAccount(long accountId) { public DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type) { SearchCriteria sc = NameServerTypeSearch.create(); sc.setParameters(ApiConstants.NAME, name); - sc.setParameters(DNS_SERVER_ID, dnsServerId); + sc.setParameters(ApiConstants.DNS_SERVER_ID, dnsServerId); sc.setParameters(ApiConstants.TYPE, type); + sc.setParameters(ApiConstants.STATE, DnsZone.State.Active); return findOneBy(sc); } @Override - public Pair, Integer> searchZones(Long id, Long dnsServerId, String keyword, Long accountId, Filter filter) { + public Pair, Integer> searchZones(Long id, Long accountId, List ownDnsServerIds, Long targetDnsServerId, + String keyword, Filter filter) { + SearchCriteria sc = AllFieldsSearch.create(); if (id != null) { sc.setParameters(ApiConstants.ID, id); } - if (dnsServerId != null) { - sc.setParameters(DNS_SERVER_ID, dnsServerId); + if (!CollectionUtils.isEmpty(ownDnsServerIds)) { + sc.setParameters(ApiConstants.DNS_SERVER_ID, ownDnsServerIds.toArray()); } if (keyword != null) { sc.setParameters(ApiConstants.NAME, "%" + keyword + "%"); @@ -89,6 +98,10 @@ public Pair, Integer> searchZones(Long id, Long dnsServerId, Str if (accountId != null) { sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); } + if (targetDnsServerId != null) { + sc.setParameters(ApiConstants.TARGET_ID, targetDnsServerId); + } + sc.setParameters(ApiConstants.STATE, DnsZone.State.Active); return searchAndCount(sc, filter); } } diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java index d27b3486612b..5ccc5bf91f9e 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java @@ -44,7 +44,7 @@ @Entity @Table(name = "dns_server") -public class DnsServerVO implements DnsServer { +public class DnsServerVO implements DnsServer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @@ -77,7 +77,7 @@ public class DnsServerVO implements DnsServer { private String externalServerId; @Column(name = "is_public") - private boolean isPublic; + private boolean publicServer; @Column(name = "public_domain_suffix") private String publicDomainSuffix; @@ -121,7 +121,7 @@ public DnsServerVO(String name, String url, Integer port, String externalServerI this.accountId = accountId; this.domainId = domainId; this.publicDomainSuffix = publicDomainSuffix; - this.isPublic = isPublic; + this.publicServer = isPublic; this.state = State.Enabled; this.nameServers = String.join(",", nameServers);; } @@ -176,8 +176,8 @@ public String getUuid() { return uuid; } - public boolean isPublic() { - return isPublic; + public boolean isPublicServer() { + return publicServer; } public State getState() { @@ -203,7 +203,7 @@ public void setNameServers(String nameServers) { } public void setIsPublic(boolean value) { - isPublic = value; + publicServer = value; } public void setPublicDomainSuffix(String publicDomainSuffix) { From 1fe79bdfb67db519725dd934fba118e7d470fab9 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 25 Feb 2026 19:52:50 +0530 Subject: [PATCH 17/23] fixes related to acl, dao --- .../api/command/user/dns/AddDnsServerCmd.java | 20 ++- .../dns/AssociateDnsZoneToNetworkCmd.java | 11 +- .../command/user/dns/DeleteDnsZoneCmd.java | 3 +- .../DisassociateDnsZoneFromNetworkCmd.java | 21 ++- .../command/user/dns/ListDnsServersCmd.java | 17 ++- .../api/command/user/dns/ListDnsZonesCmd.java | 4 +- .../user/dns/RegisterDnsRecordForVmCmd.java | 80 ------------ .../user/dns/RemoveDnsRecordForVmCmd.java | 83 ------------ .../apache/cloudstack/dns/DnsProvider.java | 1 + .../cloudstack/dns/DnsProviderManager.java | 7 - .../cloudstack/dns/DnsProviderType.java | 11 -- .../java/com/cloud/acl/DomainChecker.java | 10 +- .../dns/DnsProviderManagerImpl.java | 122 +++++++----------- .../cloudstack/dns/dao/DnsServerDaoImpl.java | 2 + .../cloudstack/dns/dao/DnsZoneDaoImpl.java | 27 ++-- 15 files changed, 126 insertions(+), 293 deletions(-) delete mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java delete mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java index 03f79a59108b..798e348e2c3e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -28,9 +28,13 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderType; import org.apache.cloudstack.dns.DnsServer; import org.apache.commons.lang3.BooleanUtils; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; + @APICommand(name = "addDnsServer", description = "Adds a new external DNS server", responseObject = DnsServerResponse.class, @@ -51,8 +55,8 @@ public class AddDnsServerCmd extends BaseCmd { @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "API URL of the provider") private String url; - @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") - private String provider; + @Parameter(name = ApiConstants.PROVIDER_TYPE, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") + private String providerType; @Parameter(name = ApiConstants.DNS_USER_NAME, type = CommandType.STRING, description = "Username or email associated with the external DNS provider account (used for authentication)") @@ -82,8 +86,9 @@ public class AddDnsServerCmd extends BaseCmd { ///////////////////////////////////////////////////// public String getName() { return name; } + public String getUrl() { return url; } - public String getProvider() { return provider; } + public String getCredentials() { return credentials; } @@ -104,6 +109,15 @@ public List getNameServers() { return nameServers; } + public DnsProviderType getProviderType() { + DnsProviderType dnsProviderType = EnumUtils.getEnumIgnoreCase(DnsProviderType.class, providerType, DnsProviderType.PowerDNS); + if (dnsProviderType == null) { + throw new InvalidParameterValueException(String.format("Invalid value passed for provider type, valid values are: %s", + EnumUtils.listValues(DnsProviderType.values()))); + } + return dnsProviderType; + } + @Override public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java index 60f9c70c9c9d..53c697db9f28 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java @@ -18,6 +18,8 @@ package org.apache.cloudstack.api.command.user.dns; import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -27,12 +29,14 @@ import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.NetworkResponse; +import org.apache.cloudstack.dns.DnsZone; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.NetworkRuleConflictException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; @APICommand(name = "associateDnsZoneToNetwork", description = "Associates a DNS Zone with a Network for VM auto-registration", @@ -47,6 +51,7 @@ public class AssociateDnsZoneToNetworkCmd extends BaseCmd { required = true, description = "The ID of the DNS zone") private Long dnsZoneId; + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, required = true, description = "The ID of the network") private Long networkId; @@ -68,7 +73,11 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE @Override public long getEntityOwnerId() { - return dnsProviderManager.getDnsZone(dnsZoneId).getAccountId(); + DnsZone zone = _entityMgr.findById(DnsZone.class, dnsZoneId); + if (zone != null) { + return zone.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; } public Long getDnsZoneId() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java index 88b3713fe2e0..fa8319a5ea20 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java @@ -80,8 +80,7 @@ public void execute() { @Override public long getEntityOwnerId() { - // Look up the Zone to find the Account Owner - DnsZone zone = dnsProviderManager.getDnsZone(id); + DnsZone zone = _entityMgr.findById(DnsZone.class, id); if (zone != null) { return zone.getAccountId(); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java index bd31737d30e9..80db66c0ec02 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java @@ -26,7 +26,8 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.NetworkResponse; import org.apache.cloudstack.api.response.SuccessResponse; import com.cloud.exception.ConcurrentOperationException; @@ -44,10 +45,14 @@ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DisassociateDnsZoneFromNetworkCmd extends BaseCmd { + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "The ID of the DNS zone") + private Long dnsZoneId; + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) - @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneNetworkMapResponse.class, - required = true, description = "The ID of the DNS zone to network mapping") - private Long id; + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, + required = true, description = "The ID of the network") + private Long networkId; @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { @@ -69,5 +74,11 @@ public long getEntityOwnerId() { return Account.ACCOUNT_ID_SYSTEM; } - public Long getId() { return id; } + public Long getDnsZoneId() { + return dnsZoneId; + } + + public void setDnsZoneId(Long dnsZoneId) { + this.dnsZoneId = dnsZoneId; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java index 39cb24a3ec33..ca7ac1944a9d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java @@ -24,8 +24,12 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsProviderType; import org.apache.cloudstack.dns.DnsServer; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; + @APICommand(name = "listDnsServers", description = "Lists DNS servers owned by the account.", responseObject = DnsServerResponse.class, @@ -43,9 +47,9 @@ public class ListDnsServersCmd extends BaseListAccountResourcesCmd { description = "the ID of the DNS server") private Long id; - @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, + @Parameter(name = ApiConstants.PROVIDER_TYPE, type = CommandType.STRING, description = "filter by provider type (e.g. PowerDNS, Cloudflare)") - private String provider; + private String providerType; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -55,8 +59,13 @@ public Long getId() { return id; } - public String getProvider() { - return provider; + public DnsProviderType getProviderType() { + DnsProviderType dnsProviderType = EnumUtils.getEnumIgnoreCase(DnsProviderType.class, providerType, DnsProviderType.PowerDNS); + if (dnsProviderType == null) { + throw new InvalidParameterValueException(String.format("Invalid value passed for provider type, valid values are: %s", + EnumUtils.listValues(DnsProviderType.values()))); + } + return dnsProviderType; } ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java index 7d6f17a28f0f..e71bdabaf617 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java @@ -20,7 +20,7 @@ import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseListAccountResourcesCmd; +import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; @@ -33,7 +33,7 @@ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) -public class ListDnsZonesCmd extends BaseListAccountResourcesCmd { +public class ListDnsZonesCmd extends BaseListCmd { ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java deleted file mode 100644 index 935c78e85fba..000000000000 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RegisterDnsRecordForVmCmd.java +++ /dev/null @@ -1,80 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.api.command.user.dns; - -import org.apache.cloudstack.acl.RoleType; -import org.apache.cloudstack.api.ACL; -import org.apache.cloudstack.api.APICommand; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.Parameter; -import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.NetworkResponse; -import org.apache.cloudstack.api.response.SuccessResponse; -import org.apache.cloudstack.api.response.UserVmResponse; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.dns.DnsRecord; - -import com.cloud.exception.ConcurrentOperationException; -import com.cloud.exception.InsufficientCapacityException; -import com.cloud.exception.NetworkRuleConflictException; -import com.cloud.exception.ResourceAllocationException; -import com.cloud.exception.ResourceUnavailableException; - -@APICommand(name = "registerDnsRecordForVm", - description = "Automatically registers a DNS record for a VM based on its associated Network and DNS Zone mapping", - responseObject = SuccessResponse.class, - entityType = {DnsRecord.class}, - since = "4.23.0", - authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) -public class RegisterDnsRecordForVmCmd extends BaseCmd { - - @ACL - @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, - required = true, description = "The ID of the Virtual Machine") - private Long vmId; - - @ACL - @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, - description = "The ID of the network. If not specified, the VM's default NIC network is used.") - private Long networkId; - - public Long getVmId() { return vmId; } - public Long getNetworkId() { return networkId; } - - @Override - public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { - try { - boolean result = dnsProviderManager.registerDnsRecordForVm(this); - if (result) { - SuccessResponse response = new SuccessResponse(getCommandName()); - setResponseObject(response); - } else { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to register DNS record for VM"); - } - } catch (Exception e) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); - } - } - - @Override - public long getEntityOwnerId() { - return CallContext.current().getCallingAccountId(); - } -} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java deleted file mode 100644 index 0d1adf7cfb03..000000000000 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/RemoveDnsRecordForVmCmd.java +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.api.command.user.dns; - -import org.apache.cloudstack.acl.RoleType; -import org.apache.cloudstack.api.ACL; -import org.apache.cloudstack.api.APICommand; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.Parameter; -import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.NetworkResponse; -import org.apache.cloudstack.api.response.SuccessResponse; -import org.apache.cloudstack.api.response.UserVmResponse; -import org.apache.cloudstack.context.CallContext; - -import com.cloud.exception.ConcurrentOperationException; -import com.cloud.exception.InsufficientCapacityException; -import com.cloud.exception.NetworkRuleConflictException; -import com.cloud.exception.ResourceAllocationException; -import com.cloud.exception.ResourceUnavailableException; - -@APICommand(name = "removeDnsRecordForVm", - description = "Removes the auto-registered DNS record for a VM", - responseObject = SuccessResponse.class, - since = "4.23.0", - authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) -public class RemoveDnsRecordForVmCmd extends BaseCmd { - - @ACL - @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, - required = true, description = "The ID of the Virtual Machine") - private Long vmId; - - @ACL - @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, - description = "The ID of the network. If not specified, the VM's default NIC network is used.") - private Long networkId; - - public Long getVmId() { - return vmId; - } - - public Long getNetworkId() { - return networkId; - } - - @Override - public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { - try { - boolean result = dnsProviderManager.removeDnsRecordForVm(this); - if (result) { - SuccessResponse response = new SuccessResponse(getCommandName()); - setResponseObject(response); - } else { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to remove DNS record for VM"); - } - } catch (Exception e) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); - } - } - - @Override - public long getEntityOwnerId() { - return CallContext.current().getCallingAccountId(); - } -} 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 852df8dd0bf7..809dad4f6474 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -24,6 +24,7 @@ import com.cloud.utils.component.Adapter; public interface DnsProvider extends Adapter { + 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 72a793328af0..e3db63a35312 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -29,8 +29,6 @@ import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; -import org.apache.cloudstack.api.command.user.dns.RegisterDnsRecordForVmCmd; -import org.apache.cloudstack.api.command.user.dns.RemoveDnsRecordForVmCmd; import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; import org.apache.cloudstack.api.command.user.dns.UpdateDnsZoneCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; @@ -56,7 +54,6 @@ public interface DnsProviderManager extends Manager, PluggableService { // Calls the Plugin (State: Inactive -> Active) DnsZone provisionDnsZone(long zoneId); - DnsZone getDnsZone(Long id); DnsZone updateDnsZone(UpdateDnsZoneCmd cmd); boolean deleteDnsZone(Long id); ListResponse listDnsZones(ListDnsZonesCmd cmd); @@ -75,9 +72,5 @@ public interface DnsProviderManager extends Manager, PluggableService { boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd); - boolean registerDnsRecordForVm(RegisterDnsRecordForVmCmd cmd); - boolean removeDnsRecordForVm(RemoveDnsRecordForVmCmd cmd); - void checkDnsServerPermissions(Account caller, DnsServer server); - void checkDnsZonePermission(Account caller, DnsZone zone); } diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java index 0b8f88d67450..23c8e936613f 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java @@ -20,15 +20,4 @@ public enum DnsProviderType { PowerDNS; // Cloudflare - - // Helper to validate and return Enum from String safely - public static DnsProviderType fromString(String type) { - if (type == null) return null; - for (DnsProviderType t : DnsProviderType.values()) { - if (t.name().equalsIgnoreCase(type)) { - return t; - } - } - throw new IllegalArgumentException("Invalid DNS Provider type: " + type); - } } diff --git a/server/src/main/java/com/cloud/acl/DomainChecker.java b/server/src/main/java/com/cloud/acl/DomainChecker.java index ba4f5e530311..b9a017fd4ee3 100644 --- a/server/src/main/java/com/cloud/acl/DomainChecker.java +++ b/server/src/main/java/com/cloud/acl/DomainChecker.java @@ -27,16 +27,15 @@ import org.apache.cloudstack.acl.RolePermissionEntity; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.affinity.AffinityGroup; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.dns.DnsServer; -import org.apache.cloudstack.dns.DnsZone; import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; import org.springframework.stereotype.Component; -import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; -import org.apache.cloudstack.backup.BackupOffering; import com.cloud.dc.DataCenter; import com.cloud.dc.DedicatedResourceVO; import com.cloud.dc.dao.DedicatedResourceDao; @@ -223,10 +222,7 @@ public boolean checkAccess(Account caller, ControlledEntity entity, AccessType a return false; } else if (entity instanceof DnsServer) { dnsProviderManager.checkDnsServerPermissions(caller, (DnsServer) entity); - } else if (entity instanceof DnsZone) { - dnsProviderManager.checkDnsZonePermission(caller, (DnsZone) entity); - } - else { + } else { validateCallerHasAccessToEntityOwner(caller, entity, accessType); } return true; 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 b79a59342dfb..5e70ec3c1f78 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import javax.inject.Inject; @@ -37,8 +38,6 @@ import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; -import org.apache.cloudstack.api.command.user.dns.RegisterDnsRecordForVmCmd; -import org.apache.cloudstack.api.command.user.dns.RemoveDnsRecordForVmCmd; import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; import org.apache.cloudstack.api.command.user.dns.UpdateDnsZoneCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; @@ -55,7 +54,6 @@ import org.apache.cloudstack.dns.vo.DnsZoneVO; import org.springframework.stereotype.Component; -import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; @@ -98,7 +96,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa @Inject DomainDao domainDao; - private DnsProvider getProvider(DnsProviderType type) { + private DnsProvider getProviderByType(DnsProviderType type) { if (type == null) { throw new CloudRuntimeException("Provider type cannot be null"); } @@ -131,13 +129,13 @@ public DnsServer addDnsServer(AddDnsServerCmd cmd) { publicDomainSuffix = DnsProviderUtil.normalizeDomain(publicDomainSuffix); } - DnsProviderType type = DnsProviderType.fromString(cmd.getProvider()); + DnsProviderType type = cmd.getProviderType(); DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), cmd.getPort(), cmd.getExternalServerId(), type, cmd.getDnsUserName(), cmd.getCredentials(), isDnsPublic, publicDomainSuffix, cmd.getNameServers(), caller.getAccountId(), caller.getDomainId()); try { - DnsProvider provider = getProvider(type); - String dnsServerId = provider.validateAndResolveServer(server); // localhost for PowerDNS + DnsProvider provider = getProviderByType(type); + String dnsServerId = provider.validateAndResolveServer(server); // returns localhost for PowerDNS if (StringUtils.isNotBlank(dnsServerId)) { server.setExternalServerId(dnsServerId); } @@ -176,57 +174,51 @@ private Pair, Integer> searchForDnsServerInternal(ListDnsServe domainId = domainIdRecursiveListProject.first(); isRecursive = domainIdRecursiveListProject.second(); Project.ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); - Filter searchFilter = new Filter(DnsServerVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + Filter searchFilter = new Filter(DnsServerVO.class, ApiConstants.ID, true, cmd.getStartIndex(), cmd.getPageSizeVal()); // Step 2: Search for caller's own DNS servers using standard ACL pattern SearchBuilder sb = dnsServerDao.createSearchBuilder(); accountMgr.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); - sb.and("state", sb.entity().getState(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.STATE, sb.entity().getState(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.PROVIDER_TYPE, sb.entity().getProviderType(), SearchCriteria.Op.EQ); sb.done(); SearchCriteria sc = sb.create(); accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); - sc.setParameters("state", DnsServer.State.Enabled); + sc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled); + if (cmd.getProviderType() != null) { + sc.setParameters(ApiConstants.PROVIDER_TYPE, cmd.getProviderType()); + } Pair, Integer> ownServersPair = dnsServerDao.searchAndCount(sc, searchFilter); List dnsServers = new ArrayList<>(ownServersPair.first()); int count = ownServersPair.second(); - - // Step 3: Search for public DNS servers from caller's domain and children - // domains - Long callerDomainId = caller.getDomainId(); - DomainVO callerDomain = domainDao.findById(callerDomainId); - if (callerDomain != null) { - List domainIds = new ArrayList<>(); - domainIds.add(callerDomainId); - List childDomains = domainDao.findAllChildren(callerDomain.getPath(), callerDomainId); - for (DomainVO childDomain : childDomains) { - domainIds.add(childDomain.getId()); - } - - SearchBuilder publicSb = dnsServerDao.createSearchBuilder(); - publicSb.and("publicDns", publicSb.entity().isPublicServer(), SearchCriteria.Op.EQ); - publicSb.and("publicDomainId", publicSb.entity().getDomainId(), SearchCriteria.Op.IN); - publicSb.and("publicState", publicSb.entity().getState(), SearchCriteria.Op.EQ); - publicSb.done(); - - SearchCriteria publicSc = publicSb.create(); - publicSc.setParameters("publicDns", 1); - publicSc.setParameters("publicDomainId", domainIds.toArray()); - publicSc.setParameters("publicState", DnsServer.State.Enabled); - - List publicServers = dnsServerDao.search(publicSc, null); - - // Deduplicate: add only public servers not already in the own servers list - List ownServerIds = dnsServers.stream().map(DnsServerVO::getId).collect(Collectors.toList()); - for (DnsServerVO publicServer : publicServers) { - if (!ownServerIds.contains(publicServer.getId())) { - dnsServers.add(publicServer); - count++; + if (cmd.getId() == null) { + Set parentDomainIds = domainDao.getDomainParentIds(caller.getDomainId()); + if (!parentDomainIds.isEmpty()) { + SearchBuilder publicSb = dnsServerDao.createSearchBuilder(); + publicSb.and(ApiConstants.IS_PUBLIC, publicSb.entity().isPublicServer(), SearchCriteria.Op.EQ); + publicSb.and(ApiConstants.DOMAIN_IDS, publicSb.entity().getDomainId(), SearchCriteria.Op.IN); + publicSb.and(ApiConstants.STATE, publicSb.entity().getState(), SearchCriteria.Op.EQ); + publicSb.and(ApiConstants.PROVIDER_TYPE, publicSb.entity().getProviderType(), SearchCriteria.Op.EQ); + publicSb.done(); + SearchCriteria publicSc = publicSb.create(); + publicSc.setParameters(ApiConstants.IS_PUBLIC, 1); + publicSc.setParameters(ApiConstants.DOMAIN_IDS, parentDomainIds.toArray()); + publicSc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled); + if (cmd.getProviderType() != null) { + publicSc.setParameters(ApiConstants.PROVIDER_TYPE, cmd.getProviderType()); + } + List publicServers = dnsServerDao.search(publicSc, null); + List ownServerIds = dnsServers.stream().map(DnsServerVO::getId).collect(Collectors.toList()); + for (DnsServerVO publicServer : publicServers) { + if (!ownServerIds.contains(publicServer.getId())) { + dnsServers.add(publicServer); + count++; + } } } } - return new Pair<>(dnsServers, count); } @@ -284,7 +276,7 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { } if (validationRequired) { - DnsProvider provider = getProvider(dnsServer.getProviderType()); + DnsProvider provider = getProviderByType(dnsServer.getProviderType()); try { provider.validate(dnsServer); } catch (Exception ex) { @@ -340,7 +332,7 @@ public boolean deleteDnsZone(Long zoneId) { DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); if (server != null && zone.getState() == DnsZone.State.Active) { try { - DnsProvider provider = getProvider(server.getProviderType()); + DnsProvider provider = getProviderByType(server.getProviderType()); provider.deleteZone(server, zone); logger.debug("Deleted DNS zone: {}", zone.getName()); } catch (Exception ex) { @@ -351,11 +343,6 @@ public boolean deleteDnsZone(Long zoneId) { return dnsZoneDao.remove(zoneId); } - @Override - public DnsZone getDnsZone(Long id) { - return dnsZoneDao.findById(id); - } - @Override public DnsZone updateDnsZone(UpdateDnsZoneCmd cmd) { DnsZoneVO dnsZone = dnsZoneDao.findById(cmd.getId()); @@ -376,7 +363,7 @@ public DnsZone updateDnsZone(UpdateDnsZoneCmd cmd) { throw new CloudRuntimeException("The underlying DNS server for this DNS zone is missing."); } try { - DnsProvider provider = getProvider(server.getProviderType()); + DnsProvider provider = getProviderByType(server.getProviderType()); provider.updateZone(server, dnsZone); } catch (Exception ex) { logger.error("Failed to update DNS zone: {} on DNS server: {}", dnsZone.getName(), server.getName(), ex); @@ -438,7 +425,7 @@ public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { 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 = getProvider(server.getProviderType()); + DnsProvider provider = getProviderByType(server.getProviderType()); String normalizedRecordName = provider.addRecord(server, zone, record); record.setName(normalizedRecordName); return createDnsRecordResponse(record); @@ -465,7 +452,7 @@ public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) { DnsRecord record = new DnsRecord(); record.setName(cmd.getName()); record.setType(cmd.getType()); - DnsProvider provider = getProvider(server.getProviderType()); + DnsProvider provider = getProviderByType(server.getProviderType()); provider.deleteRecord(server, zone, record); return true; } catch (Exception ex) { @@ -487,7 +474,7 @@ public ListResponse listDnsRecords(ListDnsRecordsCmd cmd) { throw new CloudRuntimeException("The underlying DNS server for this DNS zone is missing."); } try { - DnsProvider provider = getProvider(server.getProviderType()); + DnsProvider provider = getProviderByType(server.getProviderType()); List records = provider.listRecords(server, zone); List responses = new ArrayList<>(); for (DnsRecord record : records) { @@ -550,7 +537,7 @@ public DnsZone provisionDnsZone(long dnsZoneId) { } DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId()); try { - DnsProvider provider = getProvider(server.getProviderType()); + DnsProvider provider = getProviderByType(server.getProviderType()); String externalReferenceId = provider.provisionZone(server, dnsZone); dnsZone.setExternalReference(externalReferenceId); dnsZone.setState(DnsZone.State.Active); @@ -619,7 +606,8 @@ public DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetwor @Override public boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd) { - DnsZoneNetworkMapVO mapping = dnsZoneNetworkMapDao.findById(cmd.getId()); + // fix this method + DnsZoneNetworkMapVO mapping = dnsZoneNetworkMapDao.findById(cmd.getDnsZoneId()); if (mapping == null) { throw new InvalidParameterValueException("The specified DNS zone to network mapping does not exist."); } @@ -634,15 +622,6 @@ public boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd return dnsZoneNetworkMapDao.remove(mapping.getId()); } - @Override - public boolean registerDnsRecordForVm(RegisterDnsRecordForVmCmd cmd) { - return processDnsRecordForInstance(cmd.getVmId(), cmd.getNetworkId(), true); - } - - @Override - public boolean removeDnsRecordForVm(RemoveDnsRecordForVmCmd cmd) { - return processDnsRecordForInstance(cmd.getVmId(), cmd.getNetworkId(), false); - } @Override public void checkDnsServerPermissions(Account caller, DnsServer server) { @@ -653,18 +632,11 @@ public void checkDnsServerPermissions(Account caller, DnsServer server) { throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + server.getName()); } Account owner = accountMgr.getAccount(server.getAccountId()); - if (!domainDao.isChildDomain(owner.getDomainId(), caller.getDomainId())) { + if (!domainDao.isChildDomain(caller.getDomainId(), owner.getDomainId())) { throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + server.getName()); } } - @Override - public void checkDnsZonePermission(Account caller, DnsZone zone) { - if (caller.getId() != zone.getAccountId()) { - throw new PermissionDeniedException(caller + "is not allowed to access the DNS Zone " + zone.getName()); - } - } - /** * Helper method to handle both Register and Remove logic for Instance */ @@ -716,7 +688,7 @@ private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boo } try { - DnsProvider provider = getProvider(server.getProviderType()); + DnsProvider provider = getProviderByType(server.getProviderType()); // Handle IPv4 (A Record) if (nic.getIPv4Address() != null) { DnsRecord recordA = new DnsRecord(recordName, DnsRecord.RecordType.A, Collections.singletonList(nic.getIPv4Address()), 3600); @@ -789,8 +761,6 @@ public List> getCommands() { cmdList.add(CreateDnsRecordCmd.class); cmdList.add(ListDnsRecordsCmd.class); cmdList.add(DeleteDnsRecordCmd.class); - cmdList.add(RegisterDnsRecordForVmCmd.class); - cmdList.add(RemoveDnsRecordForVmCmd.class); return cmdList; } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java index 8a7311e2f1a9..655cdf9a3e80 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java @@ -56,6 +56,7 @@ public DnsServerDaoImpl() { DnsServerIdsByAccountSearch = createSearchBuilder(Long.class); DnsServerIdsByAccountSearch.selectFields(DnsServerIdsByAccountSearch.entity().getId()); DnsServerIdsByAccountSearch.and(ApiConstants.ACCOUNT_ID, DnsServerIdsByAccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + DnsServerIdsByAccountSearch.and(ApiConstants.STATE, DnsServerIdsByAccountSearch.entity().getState(), SearchCriteria.Op.EQ); DnsServerIdsByAccountSearch.done(); } @@ -74,6 +75,7 @@ public List listDnsServerIdsByAccountId(Long accountId) { if (accountId != null) { sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); } + sc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled); return customSearch(sc, null); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java index 9ad1217cb711..1aad6fa0f0be 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java @@ -35,7 +35,6 @@ public class DnsZoneDaoImpl extends GenericDaoBase implements DnsZoneDao { SearchBuilder AccountSearch; SearchBuilder NameServerTypeSearch; - SearchBuilder AllFieldsSearch; public DnsZoneDaoImpl() { super(); @@ -51,16 +50,6 @@ public DnsZoneDaoImpl() { NameServerTypeSearch.and(ApiConstants.TYPE, NameServerTypeSearch.entity().getType(), SearchCriteria.Op.EQ); NameServerTypeSearch.and(ApiConstants.STATE, NameServerTypeSearch.entity().getState(), SearchCriteria.Op.EQ); NameServerTypeSearch.done(); - - AllFieldsSearch = createSearchBuilder(); - AllFieldsSearch.and().op(ApiConstants.DNS_SERVER_ID, AllFieldsSearch.entity().getDnsServerId(), SearchCriteria.Op.IN); - AllFieldsSearch.or(ApiConstants.ACCOUNT_ID, AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ); - AllFieldsSearch.cp(); - AllFieldsSearch.and(ApiConstants.STATE, AllFieldsSearch.entity().getState(), SearchCriteria.Op.EQ); - AllFieldsSearch.and(ApiConstants.ID, AllFieldsSearch.entity().getId(), SearchCriteria.Op.EQ); - AllFieldsSearch.and(ApiConstants.NAME, AllFieldsSearch.entity().getName(), SearchCriteria.Op.LIKE); - AllFieldsSearch.and(ApiConstants.TARGET_ID, AllFieldsSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); - AllFieldsSearch.done(); } @Override @@ -85,7 +74,21 @@ public DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone. public Pair, Integer> searchZones(Long id, Long accountId, List ownDnsServerIds, Long targetDnsServerId, String keyword, Filter filter) { - SearchCriteria sc = AllFieldsSearch.create(); + SearchBuilder sb = createSearchBuilder(); + sb.and(ApiConstants.STATE, sb.entity().getState(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.ID, sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.NAME, sb.entity().getName(), SearchCriteria.Op.LIKE); + sb.and(ApiConstants.TARGET_ID, sb.entity().getDnsServerId(), SearchCriteria.Op.EQ); + if (!CollectionUtils.isEmpty(ownDnsServerIds)) { + sb.and().op(ApiConstants.DNS_SERVER_ID, sb.entity().getDnsServerId(), SearchCriteria.Op.IN); + sb.or(ApiConstants.ACCOUNT_ID, sb.entity().getAccountId(), SearchCriteria.Op.EQ); + sb.cp(); + } else { + sb.and(ApiConstants.ACCOUNT_ID, sb.entity().getAccountId(), SearchCriteria.Op.EQ); + } + sb.done(); + + SearchCriteria sc = sb.create(); if (id != null) { sc.setParameters(ApiConstants.ID, id); } From 6ca9d5ace8fe2d8dc65ce747717e8631528b29f1 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Thu, 26 Feb 2026 13:05:08 +0530 Subject: [PATCH 18/23] add views for dns_server and dns_zone --- .../api/response/DnsServerResponse.java | 29 +++- .../api/response/DnsZoneResponse.java | 45 ++++- ...spring-engine-schema-core-daos-context.xml | 6 + .../db/views/cloud.dns_server_view.sql | 44 +++++ .../META-INF/db/views/cloud.dns_zone_view.sql | 45 +++++ .../dns/DnsProviderManagerImpl.java | 102 ++++++++---- .../cloudstack/dns/dao/DnsServerJoinDao.java | 26 +++ .../dns/dao/DnsServerJoinDaoImpl.java | 25 +++ .../cloudstack/dns/dao/DnsZoneJoinDao.java | 26 +++ .../dns/dao/DnsZoneJoinDaoImpl.java | 25 +++ .../cloudstack/dns/vo/DnsServerJoinVO.java | 154 ++++++++++++++++++ .../cloudstack/dns/vo/DnsZoneJoinVO.java | 130 +++++++++++++++ .../spring-server-core-managers-context.xml | 4 - 13 files changed, 618 insertions(+), 43 deletions(-) create mode 100644 engine/schema/src/main/resources/META-INF/db/views/cloud.dns_server_view.sql create mode 100644 engine/schema/src/main/resources/META-INF/db/views/cloud.dns_zone_view.sql create mode 100644 server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDao.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDaoImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDao.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDaoImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerJoinVO.java create mode 100644 server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneJoinVO.java diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java index f96e05820f4e..d67be31504df 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java @@ -22,7 +22,6 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.EntityReference; -import org.apache.cloudstack.dns.DnsProviderType; import org.apache.cloudstack.dns.DnsServer; import com.cloud.serializer.Param; @@ -49,7 +48,7 @@ public class DnsServerResponse extends BaseResponse { @SerializedName(ApiConstants.PROVIDER) @Param(description = "The provider type of the DNS server") - private DnsProviderType provider; + private String provider; @SerializedName(ApiConstants.IS_PUBLIC) @Param(description = "Is the DNS server publicly available") @@ -63,6 +62,18 @@ public class DnsServerResponse extends BaseResponse { @Param(description = "Name servers entries associated to DNS server") private List nameServers; + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "the account associated with the DNS server") + private String accountName; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "the ID of the domain associated with the DNS server") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "the name of the domain associated with the DNS server") + private String domainName; + public DnsServerResponse() { super(); @@ -80,7 +91,7 @@ public void setUrl(String url) { this.url = url; } - public void setProvider(DnsProviderType provider) { + public void setProvider(String provider) { this.provider = provider; } @@ -99,4 +110,16 @@ public void setPublicDomainSuffix(String publicDomainSuffix) { public void setNameServers(List nameServers) { this.nameServers = nameServers; } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java index 4bad0b513ea4..e179d2fd4b13 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java @@ -37,7 +37,27 @@ public class DnsZoneResponse extends BaseResponse { @SerializedName("dnsserverid") @Param(description = "ID of the DNS server this zone belongs to") - private Long dnsServerId; + private String dnsServerId; + + @SerializedName("dnsservername") + @Param(description = "the name of the DNS server hosting this zone") + private String dnsServerName; + + @SerializedName("dnsserveraccount") + @Param(description = "the account name of the DNS server owner") + private String dnsServerAccountName; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "the account associated with the DNS zone") + private String accountName; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "the name of the domain associated with the DNS zone") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "the ID of the domain associated with the DNS server") + private String domainId; @SerializedName(ApiConstants.NETWORK_ID) @Param(description = "ID of the network this zone is associated with") @@ -68,7 +88,7 @@ public void setName(String name) { this.name = name; } - public void setDnsServerId(Long dnsServerId) { + public void setDnsServerId(String dnsServerId) { this.dnsServerId = dnsServerId; } @@ -95,4 +115,25 @@ public void setId(String id) { public void setDescription(String description) { this.description = description; } + + public void setDnsServerName(String dnsServerName) { + this.dnsServerName = dnsServerName; + } + + public void setDnsServerAccountName(String dnsServerAccountName) { + this.dnsServerAccountName = dnsServerAccountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + } 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 0656d5e3c440..191507e04563 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 @@ -310,4 +310,10 @@ + + + + + + diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_server_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_server_view.sql new file mode 100644 index 000000000000..a1bc1a1141a9 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_server_view.sql @@ -0,0 +1,44 @@ +-- 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_server_view`; + +DROP VIEW IF EXISTS `cloud`.`dns_server_view`; +CREATE VIEW `cloud`.`dns_server_view` AS + SELECT + dns.id, + dns.uuid, + dns.name, + dns.provider_type, + dns.url, + dns.port, + dns.name_servers, + dns.is_public, + dns.public_domain_suffix, + dns.state, + dns.created, + dns.removed, + account.account_name account_name, + domain.name domain_name, + domain.uuid domain_uuid, + domain.path domain_path + FROM + `cloud`.`dns_server` dns + INNER JOIN + `cloud`.`account` account ON dns.account_id = account.id + INNER JOIN + `cloud`.`domain` domain ON dns.domain_id = domain.id; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_zone_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_zone_view.sql new file mode 100644 index 000000000000..a41e003ae4f2 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_zone_view.sql @@ -0,0 +1,45 @@ +-- 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_zone_view`; + +DROP VIEW IF EXISTS `cloud`.`dns_zone_view`; +CREATE VIEW `cloud`.`dns_zone_view` AS + SELECT + zone.id, + zone.uuid, + zone.name, + zone.dns_server_id, + zone.state, + zone.description, + server.uuid dns_server_uuid, + server.name dns_server_name, + server_account.account_name dns_server_account_name, + account.account_name account_name, + domain.name domain_name, + domain.uuid domain_uuid, + domain.path domain_path + FROM + `cloud`.`dns_zone` zone + INNER JOIN + `cloud`.`dns_server` server ON zone.dns_server_id = server.id + INNER JOIN + `cloud`.`account` server_account ON server.account_id = server_account.id + INNER JOIN + `cloud`.`account` account ON zone.account_id = account.id + INNER JOIN + `cloud`.`domain` domain ON zone.domain_id = domain.id; 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 5e70ec3c1f78..0da957cac767 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -47,9 +47,14 @@ import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.dns.dao.DnsServerDao; +import org.apache.cloudstack.dns.dao.DnsServerJoinDao; import org.apache.cloudstack.dns.dao.DnsZoneDao; +import org.apache.cloudstack.dns.dao.DnsZoneJoinDao; import org.apache.cloudstack.dns.dao.DnsZoneNetworkMapDao; +import org.apache.cloudstack.dns.exception.DnsNotFoundException; +import org.apache.cloudstack.dns.vo.DnsServerJoinVO; import org.apache.cloudstack.dns.vo.DnsServerVO; +import org.apache.cloudstack.dns.vo.DnsZoneJoinVO; import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; import org.apache.cloudstack.dns.vo.DnsZoneVO; import org.springframework.stereotype.Component; @@ -95,6 +100,10 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa NicDao nicDao; @Inject DomainDao domainDao; + @Inject + DnsZoneJoinDao dnsZoneJoinDao; + @Inject + DnsServerJoinDao dnsServerJoinDao; private DnsProvider getProviderByType(DnsProviderType type) { if (type == null) { @@ -149,9 +158,14 @@ public DnsServer addDnsServer(AddDnsServerCmd cmd) { @Override public ListResponse listDnsServers(ListDnsServersCmd cmd) { Pair, Integer> result = searchForDnsServerInternal(cmd); + List serverIds = new ArrayList<>(); + for (DnsServer server : result.first()) { + serverIds.add(server.getUuid()); + } + List joinResult = dnsServerJoinDao.listByUuids(serverIds); ListResponse response = new ListResponse<>(); List serverResponses = new ArrayList<>(); - for (DnsServerVO server : result.first()) { + for (DnsServerJoinVO server : joinResult) { serverResponses.add(createDnsServerResponse(server)); } response.setResponses(serverResponses, result.second()); @@ -186,9 +200,7 @@ private Pair, Integer> searchForDnsServerInternal(ListDnsServe SearchCriteria sc = sb.create(); accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); sc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled); - if (cmd.getProviderType() != null) { - sc.setParameters(ApiConstants.PROVIDER_TYPE, cmd.getProviderType()); - } + sc.setParameters(ApiConstants.PROVIDER_TYPE, cmd.getProviderType()); Pair, Integer> ownServersPair = dnsServerDao.searchAndCount(sc, searchFilter); List dnsServers = new ArrayList<>(ownServersPair.first()); @@ -206,9 +218,7 @@ private Pair, Integer> searchForDnsServerInternal(ListDnsServe publicSc.setParameters(ApiConstants.IS_PUBLIC, 1); publicSc.setParameters(ApiConstants.DOMAIN_IDS, parentDomainIds.toArray()); publicSc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled); - if (cmd.getProviderType() != null) { - publicSc.setParameters(ApiConstants.PROVIDER_TYPE, cmd.getProviderType()); - } + publicSc.setParameters(ApiConstants.PROVIDER_TYPE, cmd.getProviderType()); List publicServers = dnsServerDao.search(publicSc, null); List ownServerIds = dnsServers.stream().map(DnsServerVO::getId).collect(Collectors.toList()); for (DnsServerVO publicServer : publicServers) { @@ -305,21 +315,6 @@ public boolean deleteDnsServer(DeleteDnsServerCmd cmd) { return dnsServerDao.remove(dnsServerId); } - @Override - public DnsServerResponse createDnsServerResponse(DnsServer server) { - DnsServerResponse response = new DnsServerResponse(); - response.setId(server.getUuid()); - response.setName(server.getName()); - response.setUrl(server.getUrl()); - response.setPort(server.getPort()); - response.setProvider(server.getProviderType()); - response.setPublic(server.isPublicServer()); - response.setNameServers(server.getNameServers()); - response.setPublicDomainSuffix(server.getPublicDomainSuffix()); - response.setObjectName("dnsserver"); - return response; - } - @Override public boolean deleteDnsZone(Long zoneId) { DnsZoneVO zone = dnsZoneDao.findById(zoneId); @@ -376,10 +371,14 @@ public DnsZone updateDnsZone(UpdateDnsZoneCmd cmd) { @Override public ListResponse listDnsZones(ListDnsZonesCmd cmd) { Pair, Integer> result = searchForDnsZonesInternal(cmd); - - List zoneResponses = new ArrayList<>(); + List zoneIds = new ArrayList<>(); for (DnsZoneVO zone : result.first()) { - zoneResponses.add(createDnsZoneResponse(zone)); + zoneIds.add(zone.getUuid()); + } + List zoneJoinVos = dnsZoneJoinDao.listByUuids(zoneIds); + List zoneResponses = new ArrayList<>(); + for (DnsZoneJoinVO zoneJoin: zoneJoinVos) { + zoneResponses.add(createDnsZoneResponse(zoneJoin)); } ListResponse response = new ListResponse<>(); response.setResponses(zoneResponses, result.second()); @@ -484,6 +483,9 @@ public ListResponse listDnsRecords(ListDnsRecordsCmd cmd) { ListResponse listResponse = new ListResponse<>(); listResponse.setResponses(responses, responses.size()); return listResponse; + } catch (DnsNotFoundException ex) { + logger.error("DNS zone is not found", ex); + throw new CloudRuntimeException("DNS zone is not found, please register it first"); } catch (Exception ex) { logger.error("Failed to list DNS records from provider", ex); throw new CloudRuntimeException("Failed to fetch DNS records"); @@ -551,16 +553,48 @@ public DnsZone provisionDnsZone(long dnsZoneId) { return dnsZone; } + + public DnsServerResponse createDnsServerResponse(DnsServer dnsServer) { + DnsServerJoinVO serverJoin = dnsServerJoinDao.findById(dnsServer.getId()); + return createDnsServerResponse(serverJoin); + } + + DnsServerResponse createDnsServerResponse(DnsServerJoinVO server) { + DnsServerResponse response = new DnsServerResponse(); + response.setId(server.getUuid()); + response.setName(server.getName()); + response.setUrl(server.getUrl()); + response.setPort(server.getPort()); + response.setProvider(server.getProviderType()); + response.setPublic(server.isPublicServer()); + response.setNameServers(server.getNameServers()); + response.setPublicDomainSuffix(server.getPublicDomainSuffix()); + response.setAccountName(server.getAccountName()); + response.setDomainId(server.getDomainUuid()); // Note: APIs always return UUIDs, not internal DB IDs! + response.setDomainName(server.getDomainName()); + response.setObjectName("dnsserver"); + return response; + } + @Override - public DnsZoneResponse createDnsZoneResponse(DnsZone zone) { - DnsZoneResponse res = new DnsZoneResponse(); - res.setName(zone.getName()); - res.setDnsServerId(zone.getDnsServerId()); - res.setType(zone.getType()); - res.setState(zone.getState()); - res.setId(zone.getUuid()); - res.setDescription(zone.getDescription()); - return res; + public DnsZoneResponse createDnsZoneResponse(DnsZone dnsZone) { + DnsZoneJoinVO zoneJoinVO = dnsZoneJoinDao.findById(dnsZone.getId()); + return createDnsZoneResponse(zoneJoinVO); + } + + DnsZoneResponse createDnsZoneResponse(DnsZoneJoinVO zone) { + DnsZoneResponse response = new DnsZoneResponse(); + response.setId(zone.getUuid()); + response.setName(zone.getName()); + response.setDnsServerId(zone.getDnsServerUuid()); + response.setAccountName(zone.getAccountName()); + response.setDomainId(zone.getDomainUuid()); + response.setDomainName(zone.getDomainName()); + response.setDnsServerName(zone.getDnsServerName()); + response.setDnsServerAccountName(zone.getDnsServerAccountName()); + response.setState(zone.getState()); + response.setDescription(zone.getDescription()); + return response; } @Override diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDao.java new file mode 100644 index 000000000000..b1932d6709cb --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDao.java @@ -0,0 +1,26 @@ +// 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 org.apache.cloudstack.dns.vo.DnsServerJoinVO; + +import com.cloud.utils.db.GenericDao; + +public interface DnsServerJoinDao extends GenericDao { + +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDaoImpl.java new file mode 100644 index 000000000000..28a2e9d6638a --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDaoImpl.java @@ -0,0 +1,25 @@ +// 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 org.apache.cloudstack.dns.vo.DnsServerJoinVO; + +import com.cloud.utils.db.GenericDaoBase; + +public class DnsServerJoinDaoImpl extends GenericDaoBase implements DnsServerJoinDao { +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDao.java new file mode 100644 index 000000000000..ee0bcc5bcae2 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDao.java @@ -0,0 +1,26 @@ +// 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 org.apache.cloudstack.dns.vo.DnsZoneJoinVO; + +import com.cloud.utils.db.GenericDao; + +public interface DnsZoneJoinDao extends GenericDao { + +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDaoImpl.java new file mode 100644 index 000000000000..6be2a3a798e3 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDaoImpl.java @@ -0,0 +1,25 @@ +// 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 org.apache.cloudstack.dns.vo.DnsZoneJoinVO; + +import com.cloud.utils.db.GenericDaoBase; + +public class DnsZoneJoinDaoImpl extends GenericDaoBase implements DnsZoneJoinDao { +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerJoinVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerJoinVO.java new file mode 100644 index 000000000000..ae59efc51c21 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerJoinVO.java @@ -0,0 +1,154 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +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 org.apache.cloudstack.dns.DnsServer; + +import com.cloud.api.query.vo.BaseViewVO; +import com.cloud.utils.StringUtils; + +@Entity +@Table(name = "dns_server_view") +public class DnsServerJoinVO extends BaseViewVO implements InternalIdentity, Identity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "provider_type") + private String providerType; + + @Column(name = "url") + private String url; + + @Column(name = "port") + private Integer port; + + @Column(name = "name_servers") + private String nameServers; + + @Column(name = "is_public") + private boolean isPublic; + + @Column(name = "public_domain_suffix") + private String publicDomainSuffix; + + @Column(name = "state") + @Enumerated(value = EnumType.STRING) + private DnsServer.State state; + + @Column(name = "account_name") + private String accountName; + + @Column(name = "domain_name") + private String domainName; + + @Column(name = "domain_uuid") + private String domainUuid; + + @Column(name = "domain_path") + private String domainPath; + + public DnsServerJoinVO() { + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public String getProviderType() { + return providerType; + } + + public String getUrl() { + return url; + } + + public Integer getPort() { + return port; + } + + public List getNameServers() { + if (StringUtils.isBlank(nameServers)) { + return Collections.emptyList(); + } + return Arrays.asList(nameServers.split(",")); + } + + public boolean isPublicServer() { + return isPublic; + } + + public String getPublicDomainSuffix() { + return publicDomainSuffix; + } + + public DnsServer.State getState() { + return state; + } + + public String getAccountName() { + return accountName; + } + + public String getDomainName() { + return domainName; + } + + public String getDomainPath() { + return domainPath; + } + + public String getDomainUuid() { + return domainUuid; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneJoinVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneJoinVO.java new file mode 100644 index 000000000000..4468a4f64e0e --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneJoinVO.java @@ -0,0 +1,130 @@ +/* + * 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 javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +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 org.apache.cloudstack.dns.DnsZone; + +import com.cloud.api.query.vo.BaseViewVO; + +@Entity +@Table(name = "dns_zone_view") +public class DnsZoneJoinVO extends BaseViewVO implements InternalIdentity, Identity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "state") + @Enumerated(value = EnumType.STRING) + private DnsZone.State state; + + @Column(name = "dns_server_uuid") + private String dnsServerUuid; + + @Column(name = "dns_server_name") + private String dnsServerName; + + @Column(name = "dns_server_account_name") + private String dnsServerAccountName; + + @Column(name = "account_name") + private String accountName; + + @Column(name = "domain_name") + private String domainName; + + @Column(name = "domain_uuid") + private String domainUuid; + + @Column(name = "domain_path") + private String domainPath; + + @Column(name = "description") + private String description; + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public DnsZone.State getState() { + return state; + } + + public String getDnsServerUuid() { + return dnsServerUuid; + } + + public String getDnsServerName() { + return dnsServerName; + } + + public String getAccountName() { + return accountName; + } + + public String getDomainName() { + return domainName; + } + + public String getDomainPath() { + return domainPath; + } + + public String getName() { + return name; + } + + public String getDnsServerAccountName() { + return dnsServerAccountName; + } + + public String getDomainUuid() { + return domainUuid; + } + + public String getDescription() { + return description; + } + +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 02b38e386344..1869a4862af8 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -399,10 +399,6 @@ - - - - From 1c1eef3cc7908556a424120fe53ffdabb3adc6ac Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Mon, 2 Mar 2026 17:17:13 +0530 Subject: [PATCH 19/23] Add Listener for VM lifecycle to add dnsrecords for associated dns zone --- .../cloudstack/dns/DnsProviderManager.java | 5 + .../cloudstack/inmemory/module.properties | 18 ++ .../spring-event-inmemory-context.xml | 34 +++ .../dns/DnsProviderManagerImpl.java | 48 +---- .../dns/DnsVmLifecycleListener.java | 193 ++++++++++++++++++ .../core/spring-event-bus-context.xml | 35 ++++ .../spring-server-core-managers-context.xml | 1 + 7 files changed, 297 insertions(+), 37 deletions(-) create mode 100644 plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/module.properties create mode 100644 plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/spring-event-inmemory-context.xml create mode 100644 server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java create mode 100644 server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml 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 e3db63a35312..f9b4ecfd5820 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -37,9 +37,12 @@ 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 { @@ -73,4 +76,6 @@ public interface DnsProviderManager extends Manager, PluggableService { boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd); void checkDnsServerPermissions(Account caller, DnsServer server); + + boolean processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd); } diff --git a/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/module.properties b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/module.properties new file mode 100644 index 000000000000..c33f70e8919a --- /dev/null +++ b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/module.properties @@ -0,0 +1,18 @@ +# 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. +name=inmemory +parent=event diff --git a/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/spring-event-inmemory-context.xml b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/spring-event-inmemory-context.xml new file mode 100644 index 000000000000..4b0215170639 --- /dev/null +++ b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/spring-event-inmemory-context.xml @@ -0,0 +1,34 @@ + + + + + + + + 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 0da957cac767..7d8d8f0b0f86 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -62,6 +62,7 @@ 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.projects.Project; @@ -76,8 +77,8 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.NicVO; -import com.cloud.vm.UserVmVO; +import com.cloud.vm.Nic; +import com.cloud.vm.VirtualMachine; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; @@ -671,50 +672,21 @@ public void checkDnsServerPermissions(Account caller, DnsServer server) { } } - /** - * Helper method to handle both Register and Remove logic for Instance - */ - private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boolean isAdd) { - // 1. Fetch VM and verify access - UserVmVO instance = userVmDao.findById(instanceId); - if (instance == null) { - throw new InvalidParameterValueException("Provided Instance not found."); - } - accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, instance); - - // 2. Resolve the NIC and Network - NicVO nic; - if (networkId != null) { - nic = nicDao.findByNtwkIdAndInstanceId(networkId, instance.getId()); - } else { - nic = nicDao.findDefaultNicForVM(instance.getId()); - networkId = nic != null ? nic.getNetworkId() : null; - } - - // networkId may not be of Shared network type - // there might be multiple shared networks - // possible to have dns record for secondary ip - - if (nic == null) { - throw new CloudRuntimeException("No valid NIC found for this Instance on the specified Network."); - } - - // 3. Find if this network is linked to any DNS Zones + @Override + public boolean processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd) { + long networkId = network.getId(); List mappings = dnsZoneNetworkMapDao.listByNetworkId(networkId); if (mappings == null || mappings.isEmpty()) { - throw new CloudRuntimeException("No DNS zones are mapped to this network. Please associate a zone first."); + logger.warn("No DNS zones are mapped to this network. Please associate a zone first."); + return false; } - boolean atLeastOneSuccess = false; - // 4. Iterate over mapped zones and push the record for (DnsZoneNetworkMapVO map : mappings) { DnsZoneVO zone = dnsZoneDao.findById(map.getDnsZoneId()); if (zone == null || zone.getState() != DnsZone.State.Active) { continue; } - DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); - // Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain") String recordName = String.valueOf(instance.getInstanceName()); if (StringUtils.isNotBlank(map.getSubDomain())) { @@ -753,11 +725,13 @@ private boolean processDnsRecordForInstance(Long instanceId, Long networkId, boo zone.getName(), ex ); + return false; } } if (!atLeastOneSuccess) { - throw new CloudRuntimeException("Failed to process DNS records. Ensure the Instance has a valid IP address."); + logger.error("Failed to process DNS records. Ensure the Instance has a valid IP address."); + return false; } return true; } diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java b/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java new file mode 100644 index 000000000000..5ebf5f928d55 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java @@ -0,0 +1,193 @@ +// +// 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; + +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.framework.events.Event; +import org.apache.cloudstack.framework.events.EventBus; +import org.apache.cloudstack.framework.events.EventBusException; +import org.apache.cloudstack.framework.events.EventSubscriber; +import org.apache.cloudstack.framework.events.EventTopic; +import org.springframework.stereotype.Component; + +import com.cloud.event.EventTypes; +import com.cloud.network.Network; +import com.cloud.network.dao.NetworkDao; +import com.cloud.utils.StringUtils; +import com.cloud.utils.component.ManagerBase; +import com.cloud.vm.Nic; +import com.cloud.vm.NicVO; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.VMInstanceDao; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Component +public class DnsVmLifecycleListener extends ManagerBase implements EventSubscriber { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Inject + private EventBus eventBus = null; + + @Inject + VMInstanceDao vmInstanceDao; + @Inject + NetworkDao networkDao; + @Inject + NicDao nicDao; + @Inject + DnsProviderManager providerManager; + + @Override + public boolean configure(final String name, final Map params) { + if (eventBus == null) { + logger.info("EventBus is not available; DNS Instance lifecycle listener will not subscribe to events"); + return true; + } + try { + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_CREATE, null, null, null), this); + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_STOP, null, null, null), this); + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_DESTROY, null, null, null), this); + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_CREATE, null, null, null), this); + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_DELETE, null, null, null), this); + } catch (EventBusException ex) { + logger.error("Failed to subscribe DnsVmLifecycleListener to EventBus", ex); + } + return true; + } + + @Override + public void onEvent(Event event) { + logger.debug("Received EventBus event: {}", event); + JsonNode descJson = parseEventDescription(event); + if (!isEventCompleted(descJson)) { + return; + } + + String eventType = event.getEventType(); + String resourceUuid = event.getResourceUUID(); + logger.debug("Processing Event: {}", event); + try { + switch (eventType) { + case EventTypes.EVENT_VM_CREATE: + case EventTypes.EVENT_VM_START: + handleVmEvent(resourceUuid, true); + break; + case EventTypes.EVENT_VM_STOP: + case EventTypes.EVENT_VM_DESTROY: + handleVmEvent(resourceUuid, false); + break; + case EventTypes.EVENT_NIC_CREATE: + handleNicEvent(descJson, true); + break; + case EventTypes.EVENT_NIC_DELETE: + handleNicEvent(descJson, false); + break; + default: + break; + } + } catch (Exception ex) { + logger.error("Failed to process DNS lifecycle event: type={}, resourceUuid={}", + eventType, event.getResourceUUID(), ex); + + } + } + + private void handleNicEvent(JsonNode eventDesc, boolean isAddDnsRecord) { + JsonNode nicUuid = eventDesc.get("Nic"); + JsonNode vmUuid = eventDesc.get("VirtualMachine"); + JsonNode networkUuid = eventDesc.get("Network"); + if (nicUuid == null || nicUuid.isNull() || vmUuid == null || vmUuid.isNull() || networkUuid == null || networkUuid.isNull()) { + logger.warn("Event has missing data to work on: {}", eventDesc); + return; + } + VMInstanceVO vmInstanceVO = vmInstanceDao.findByUuid(vmUuid.asText()); + if (vmInstanceVO == null) { + logger.error("Unable to find Instance with ID: {}", vmUuid); + return; + } + + Network network = networkDao.findByUuid(networkUuid.asText()); + if (network == null || !Network.GuestType.Shared.equals(network.getGuestType())) { + logger.warn("Network is not eligible for DNS record registration"); + return; + } + Nic nic = nicDao.findByUuid(nicUuid.asText()); + if (nic == null) { + logger.error("NIC is not found for the ID: {}", nicUuid); + } + + boolean dnsRecordAdded = providerManager.processDnsRecordForInstance(vmInstanceVO, network, nic, isAddDnsRecord); + if (!dnsRecordAdded) { + logger.error("Failure {} DNS record for Instance: {} for Network with ID: {}", + isAddDnsRecord ? "adding" : "removing", vmUuid, networkUuid); + } + } + + private void handleVmEvent(String vmUuid, boolean isAddDnsRecord) { + VMInstanceVO vmInstanceVO = vmInstanceDao.findByUuid(vmUuid); + if (vmInstanceVO == null) { + logger.error("Unable to find Instance with ID: {}", vmUuid); + return; + } + List vmNics = nicDao.listByVmId(vmInstanceVO.getId()); + for (NicVO nic : vmNics) { + Network network = networkDao.findById(nic.getNetworkId()); + if (Network.GuestType.Shared.equals(network.getGuestType())) { + boolean dnsRecordAdded = providerManager.processDnsRecordForInstance(vmInstanceVO, network, nic, isAddDnsRecord); + if (!dnsRecordAdded) { + logger.error("Failure {} DNS record for Instance: {} for Network with ID: {}", + isAddDnsRecord ? "adding" : "removing", vmUuid, network.getUuid()); + } + } + } + } + + private JsonNode parseEventDescription(Event event) { + String rawDescription = event.getDescription(); + if (StringUtils.isBlank(rawDescription)) { + return null; + } + try { + return OBJECT_MAPPER.readTree(rawDescription); + } catch (Exception ex) { + logger.warn("parseEventDescription: failed to parse description for event [{}]: {}", + event.getEventType(), ex.getMessage()); + return null; + } + } + + private boolean isEventCompleted(JsonNode descJson) { + if (descJson == null) { + return false; + } + JsonNode statusNode = descJson.get(ApiConstants.STATUS); + if (statusNode == null || statusNode.isNull()) { + return false; + } + return ApiConstants.COMPLETED.equalsIgnoreCase(statusNode.asText()); + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml new file mode 100644 index 000000000000..36c26050aaa6 --- /dev/null +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 1869a4862af8..f49573892590 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -402,4 +402,5 @@ + From caf85330bb7d9346129dd36c8040c6c70329078d Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 3 Mar 2026 14:08:50 +0530 Subject: [PATCH 20/23] 1. implement event processing for vm start/stop/destroy, nic create/delete events 2. add dnsrecordurl in nic_details table 3. add dnsrecordurl in vm response --- .../apache/cloudstack/api/ApiConstants.java | 1 + .../DisassociateDnsZoneFromNetworkCmd.java | 9 +- .../cloudstack/api/response/NicResponse.java | 8 ++ .../apache/cloudstack/dns/DnsProvider.java | 2 +- .../cloudstack/dns/DnsProviderManager.java | 2 +- .../META-INF/db/views/cloud.user_vm_view.sql | 4 +- .../dns/powerdns/PowerDnsClient.java | 2 +- .../dns/powerdns/PowerDnsProvider.java | 4 +- .../api/query/dao/UserVmJoinDaoImpl.java | 17 ++- .../com/cloud/api/query/vo/UserVmJoinVO.java | 12 +- .../dns/DnsProviderManagerImpl.java | 129 ++++++++---------- .../dns/DnsVmLifecycleListener.java | 54 ++++---- .../dns/dao/DnsZoneNetworkMapDao.java | 4 +- .../dns/dao/DnsZoneNetworkMapDaoImpl.java | 6 +- ui/src/views/network/NicsTable.vue | 4 + 15 files changed, 137 insertions(+), 121 deletions(-) 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 13dd0305a003..5f7a4fafb740 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1353,6 +1353,7 @@ public class ApiConstants { public static final String DISABLED = "disabled"; public static final String CONTENT_TYPE = "Content-Type"; public static final String NATIVE_ZONE = "Native"; + public static final String NIC_DNS_RECORD = "nicdnsrecord"; 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/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java index 80db66c0ec02..51808323b7c2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java @@ -45,13 +45,12 @@ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) public class DisassociateDnsZoneFromNetworkCmd extends BaseCmd { - @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, - required = true, description = "The ID of the DNS zone") + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, description = "The ID of the DNS zone") private Long dnsZoneId; @ACL(accessType = SecurityChecker.AccessType.OperateEntry) @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, - required = true, description = "The ID of the network") + required = true, description = "The ID of the Network") private Long networkId; @Override @@ -78,7 +77,7 @@ public Long getDnsZoneId() { return dnsZoneId; } - public void setDnsZoneId(Long dnsZoneId) { - this.dnsZoneId = dnsZoneId; + public Long getNetworkId() { + return networkId; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java index f992514b8db2..95b5fb401d22 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java @@ -146,6 +146,10 @@ public class NicResponse extends BaseResponse { @Param(description = "Public IP address associated with this NIC via Static NAT rule") private String publicIp; + @SerializedName("dnsrecordurl") + @Param(description = "Public IP address associated with this NIC via Static NAT rule") + private String dnsRecordUrl; + public void setVmId(String vmId) { this.vmId = vmId; } @@ -416,4 +420,8 @@ public void setPublicIpId(String publicIpId) { public void setPublicIp(String publicIp) { this.publicIp = publicIp; } + + public void setDnsRecordUrl(String dnsRecordUrl) { + this.dnsRecordUrl = dnsRecordUrl; + } } 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 809dad4f6474..56e5c5b8abf2 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -40,5 +40,5 @@ public interface DnsProvider extends Adapter { String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; List listRecords(DnsServer server, DnsZone zone) throws DnsProviderException; String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; - void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; + String deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; } 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 f9b4ecfd5820..f1af22681ba8 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -77,5 +77,5 @@ public interface DnsProviderManager extends Manager, PluggableService { void checkDnsServerPermissions(Account caller, DnsServer server); - boolean processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd); + String processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd); } diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql index 94bc8640fd54..ac2d007bd2bf 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql @@ -141,6 +141,7 @@ SELECT `nics`.`mac_address` AS `mac_address`, `nics`.`broadcast_uri` AS `broadcast_uri`, `nics`.`isolation_uri` AS `isolation_uri`, + `nic_details`.`value` AS `dns_record_url`, `vpc`.`id` AS `vpc_id`, `vpc`.`uuid` AS `vpc_uuid`, `networks`.`uuid` AS `network_uuid`, @@ -185,7 +186,7 @@ SELECT `lease_expiry_action`.`value` AS `lease_expiry_action`, `lease_action_execution`.`value` AS `lease_action_execution` FROM - (((((((((((((((((((((((((((((((((((((`user_vm` + ((((((((((((((((((((((((((((((((((((((`user_vm` JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) AND ISNULL(`vm_instance`.`removed`)))) JOIN `account` ON ((`vm_instance`.`account_id` = `account`.`id`))) @@ -212,6 +213,7 @@ FROM LEFT JOIN `user_data` ON ((`user_data`.`id` = `user_vm`.`user_data_id`))) LEFT JOIN `nics` ON (((`vm_instance`.`id` = `nics`.`instance_id`) AND ISNULL(`nics`.`removed`)))) + LEFT JOIN `nic_details` ON ((`nic_details`.`nic_id` = `nics`.`id`) AND (`nic_details`.`name` = 'nicdnsrecord'))) LEFT JOIN `networks` ON ((`nics`.`network_id` = `networks`.`id`))) LEFT JOIN `vpc` ON (((`networks`.`vpc_id` = `vpc`.`id`) AND ISNULL(`vpc`.`removed`)))) 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 27a322f7e39d..4abe2b0b02db 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 @@ -223,7 +223,7 @@ public String modifyRecord(String baseUrl, Integer port, String apiKey, String e HttpPatch request = new HttpPatch(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone)); request.setEntity(new org.apache.http.entity.StringEntity(root.toString(), StandardCharsets.UTF_8)); execute(request, apiKey, 204); - return normalizedRecord; + return normalizedRecord.endsWith(".") ? normalizedRecord.substring(0, normalizedRecord.length() - 1) : normalizedRecord; } public Iterable listRecords(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName) throws DnsProviderException { 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 44862ff2e254..897488e5a576 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 @@ -105,9 +105,9 @@ public String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) thr } @Override - public void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { + public String deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { validateRequiredServerAndZoneFields(server, zone); - applyRecord(server.getUrl(), + return applyRecord(server.getUrl(), server.getPort(), server.getApiKey(), server.getExternalServerId(), diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 93dca8cc07a1..0d09083ccdfe 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -17,14 +17,13 @@ package com.cloud.api.query.dao; import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Collections; import java.time.LocalDate; import java.time.ZoneId; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.Date; - import java.util.HashMap; import java.util.Hashtable; import java.util.List; @@ -34,8 +33,6 @@ import javax.inject.Inject; -import com.cloud.gpu.dao.VgpuProfileDao; -import com.cloud.service.dao.ServiceOfferingDao; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -61,11 +58,13 @@ import com.cloud.api.ApiResponseHelper; import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.gpu.GPU; +import com.cloud.gpu.dao.VgpuProfileDao; import com.cloud.host.ControlState; import com.cloud.network.IpAddress; import com.cloud.network.vpc.VpcVO; import com.cloud.network.vpc.dao.VpcDao; import com.cloud.service.ServiceOfferingDetailsVO; +import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOS; import com.cloud.storage.Storage.TemplateType; @@ -92,9 +91,9 @@ import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.VmStats; +import com.cloud.vm.dao.NicDetailsDao; import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.NicSecondaryIpVO; - import com.cloud.vm.dao.VMInstanceDetailsDao; @Component @@ -128,6 +127,8 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation VmDetailSearch; private final SearchBuilder activeVmByIsoSearch; @@ -358,6 +359,7 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us nicResponse.setIp6Address(userVm.getIp6Address()); nicResponse.setIp6Gateway(userVm.getIp6Gateway()); nicResponse.setIp6Cidr(userVm.getIp6Cidr()); + nicResponse.setDnsRecordUrl(userVm.getDnsRecordUrl()); if (userVm.getBroadcastUri() != null) { nicResponse.setBroadcastUri(userVm.getBroadcastUri().toString()); } @@ -611,6 +613,9 @@ public UserVmResponse setUserVmResponse(ResponseView view, UserVmResponse userVm nicResponse.setIp6Gateway(uvo.getIp6Gateway()); /*13: IPv6Cidr*/ nicResponse.setIp6Cidr(uvo.getIp6Cidr()); + /* dnsRecordUrl */ + nicResponse.setDnsRecordUrl(uvo.getDnsRecordUrl()); + /*14: deviceId*/ // where do we find nicResponse.setDeviceId( // this is probably not String.valueOf(uvo.getNicId())); as this is a db-id diff --git a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java index eab34081d514..1a6dbffe1897 100644 --- a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java @@ -32,21 +32,22 @@ import javax.persistence.TemporalType; import javax.persistence.Transient; +import org.apache.cloudstack.util.HypervisorTypeConverter; + import com.cloud.host.Status; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.Network.GuestType; import com.cloud.network.Networks.TrafficType; import com.cloud.resource.ResourceState; import com.cloud.storage.Storage; -import com.cloud.storage.Storage.TemplateType; import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Storage.TemplateType; import com.cloud.storage.Volume; import com.cloud.user.Account; import com.cloud.util.StoragePoolTypeConverter; import com.cloud.utils.db.GenericDao; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; -import org.apache.cloudstack.util.HypervisorTypeConverter; @Entity @Table(name = "user_vm_view") @@ -398,6 +399,9 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro @Column(name = "public_ip_address") private String publicIpAddress; + @Column(name = "dns_record_url") + private String dnsRecordUrl; + @Column(name = "user_data", updatable = true, nullable = true, length = 2048) private String userData; @@ -1089,4 +1093,8 @@ public void setLeaseExpiryAction(String leaseExpiryAction) { public String getLeaseActionExecution() { return leaseActionExecution; } + + public String getDnsRecordUrl() { + return dnsRecordUrl; + } } 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 7d8d8f0b0f86..a68c7d78fe98 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -441,20 +441,15 @@ public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) { if (zone == 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()); - try { - // Reconstruct the record DTO just for deletion criteria DnsRecord record = new DnsRecord(); record.setName(cmd.getName()); record.setType(cmd.getType()); DnsProvider provider = getProviderByType(server.getProviderType()); - provider.deleteRecord(server, zone, record); - return true; + return provider.deleteRecord(server, zone, record) != null; } catch (Exception ex) { logger.error("Failed to delete DNS record via provider", ex); throw new CloudRuntimeException(String.format("Failed to delete DNS record: %s", cmd.getName())); @@ -610,30 +605,30 @@ public DnsRecordResponse createDnsRecordResponse(DnsRecord record) { @Override public DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetworkCmd cmd) { Account caller = CallContext.current().getCallingAccount(); - 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."); } - accountMgr.checkAccess(caller, null, true, zone); + accountMgr.checkAccess(caller, null, true, dnsZone); NetworkVO network = networkDao.findById(cmd.getNetworkId()); if (network == null) { throw new InvalidParameterValueException("Network not found."); } - if (!NetworkVO.GuestType.Shared.equals(network.getGuestType())) { throw new CloudRuntimeException(String.format("Operation is not permitted for network type: %s", network.getGuestType())); } accountMgr.checkAccess(caller, null, true, network); - DnsZoneNetworkMapVO existing = dnsZoneNetworkMapDao.findByZoneAndNetwork(zone.getId(), network.getId()); + + DnsZoneNetworkMapVO existing = dnsZoneNetworkMapDao.findByNetworkId(network.getId()); if (existing != null) { - throw new InvalidParameterValueException("This DNS zone is already associated with this Network."); + throw new InvalidParameterValueException("Network has existing DNS zone associated to it."); } - DnsZoneNetworkMapVO mapping = new DnsZoneNetworkMapVO(zone.getId(), network.getId(), cmd.getSubDomain()); + DnsZoneNetworkMapVO mapping = new DnsZoneNetworkMapVO(dnsZone.getId(), network.getId(), cmd.getSubDomain()); dnsZoneNetworkMapDao.persist(mapping); DnsZoneNetworkMapResponse response = new DnsZoneNetworkMapResponse(); response.setId(mapping.getUuid()); - response.setDnsZoneId(zone.getUuid()); + response.setDnsZoneId(dnsZone.getUuid()); response.setNetworkId(network.getUuid()); response.setSubDomain(mapping.getSubDomain()); return response; @@ -641,10 +636,9 @@ public DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetwor @Override public boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd) { - // fix this method - DnsZoneNetworkMapVO mapping = dnsZoneNetworkMapDao.findById(cmd.getDnsZoneId()); + DnsZoneNetworkMapVO mapping = dnsZoneNetworkMapDao.findByNetworkId(cmd.getNetworkId()); if (mapping == null) { - throw new InvalidParameterValueException("The specified DNS zone to network mapping does not exist."); + throw new InvalidParameterValueException("No DNS zone is associated to specified network."); } DnsZoneVO zone = dnsZoneDao.findById(mapping.getDnsZoneId()); if (zone == null) { @@ -673,67 +667,58 @@ public void checkDnsServerPermissions(Account caller, DnsServer server) { } @Override - public boolean processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd) { + public String processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd) { long networkId = network.getId(); - List mappings = dnsZoneNetworkMapDao.listByNetworkId(networkId); - if (mappings == null || mappings.isEmpty()) { - logger.warn("No DNS zones are mapped to this network. Please associate a zone first."); - return false; - } - boolean atLeastOneSuccess = false; - for (DnsZoneNetworkMapVO map : mappings) { - DnsZoneVO zone = dnsZoneDao.findById(map.getDnsZoneId()); - if (zone == null || zone.getState() != DnsZone.State.Active) { - continue; - } - DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); - // Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain") - String recordName = String.valueOf(instance.getInstanceName()); - if (StringUtils.isNotBlank(map.getSubDomain())) { - recordName = recordName + "." + map.getSubDomain(); - } + DnsZoneNetworkMapVO dnsZoneNetworkMap = dnsZoneNetworkMapDao.findByNetworkId(networkId); + if (dnsZoneNetworkMap == null) { + logger.warn("No DNS zone is mapped to this network. Please associate a zone first."); + return null; + } + DnsZoneVO dnsZone = dnsZoneDao.findById(dnsZoneNetworkMap.getDnsZoneId()); + if (dnsZone == null || dnsZone.getState() != DnsZone.State.Active) { + return null; + } + DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId()); + // Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain") + String recordName = String.valueOf(instance.getInstanceName()); + if (StringUtils.isNotBlank(dnsZoneNetworkMap.getSubDomain())) { + recordName = recordName + "." + dnsZoneNetworkMap.getSubDomain(); + } - try { - DnsProvider provider = getProviderByType(server.getProviderType()); - // Handle IPv4 (A Record) - if (nic.getIPv4Address() != null) { - DnsRecord recordA = new DnsRecord(recordName, DnsRecord.RecordType.A, Collections.singletonList(nic.getIPv4Address()), 3600); - if (isAdd) { - provider.addRecord(server, zone, recordA); - } else { - provider.deleteRecord(server, zone, recordA); - } - atLeastOneSuccess = true; + 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 - if (nic.getIPv6Address() != null) { - DnsRecord recordAAAA = new DnsRecord(recordName, DnsRecord.RecordType.AAAA, Collections.singletonList(nic.getIPv6Address()), 3600); - if (isAdd) { - provider.addRecord(server, zone, recordAAAA); - } else { - provider.deleteRecord(server, zone, recordAAAA); - } - atLeastOneSuccess = true; + // 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); } - - } catch (Exception ex) { - logger.error( - "Failed to {} DNS record for Instance {} in zone {}", - isAdd ? "register" : "remove", - instance.getHostName(), - zone.getName(), - ex - ); - return false; } - } - - if (!atLeastOneSuccess) { - logger.error("Failed to process DNS records. Ensure the Instance has a valid IP address."); - return false; - } - return true; + return ipv4DnsRecord != null ? ipv4DnsRecord : ipv6DnsRecord; + } catch (Exception ex) { + logger.error( + "Failed to {} DNS record for Instance {} in zone {}", + isAdd ? "register" : "remove", + instance.getHostName(), + dnsZone.getName(), + ex + ); + } + return null; } @Override diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java b/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java index 5ebf5f928d55..53f6fb62105f 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java @@ -41,6 +41,7 @@ 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; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -60,6 +61,8 @@ public class DnsVmLifecycleListener extends ManagerBase implements EventSubscrib NicDao nicDao; @Inject DnsProviderManager providerManager; + @Inject + NicDetailsDao nicDetailsDao; @Override public boolean configure(final String name, final Map params) { @@ -69,6 +72,7 @@ public boolean configure(final String name, final Map params) { } try { eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_CREATE, null, null, null), this); + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_START, null, null, null), this); eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_STOP, null, null, null), this); eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_DESTROY, null, null, null), this); eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_CREATE, null, null, null), this); @@ -81,7 +85,6 @@ public boolean configure(final String name, final Map params) { @Override public void onEvent(Event event) { - logger.debug("Received EventBus event: {}", event); JsonNode descJson = parseEventDescription(event); if (!isEventCompleted(descJson)) { return; @@ -89,7 +92,6 @@ public void onEvent(Event event) { String eventType = event.getEventType(); String resourceUuid = event.getResourceUUID(); - logger.debug("Processing Event: {}", event); try { switch (eventType) { case EventTypes.EVENT_VM_CREATE: @@ -112,15 +114,13 @@ public void onEvent(Event event) { } catch (Exception ex) { logger.error("Failed to process DNS lifecycle event: type={}, resourceUuid={}", eventType, event.getResourceUUID(), ex); - } } private void handleNicEvent(JsonNode eventDesc, boolean isAddDnsRecord) { JsonNode nicUuid = eventDesc.get("Nic"); JsonNode vmUuid = eventDesc.get("VirtualMachine"); - JsonNode networkUuid = eventDesc.get("Network"); - if (nicUuid == null || nicUuid.isNull() || vmUuid == null || vmUuid.isNull() || networkUuid == null || networkUuid.isNull()) { + if (nicUuid == null || nicUuid.isNull() || vmUuid == null || vmUuid.isNull()) { logger.warn("Event has missing data to work on: {}", eventDesc); return; } @@ -129,22 +129,17 @@ private void handleNicEvent(JsonNode eventDesc, boolean isAddDnsRecord) { logger.error("Unable to find Instance with ID: {}", vmUuid); return; } - - Network network = networkDao.findByUuid(networkUuid.asText()); - if (network == null || !Network.GuestType.Shared.equals(network.getGuestType())) { - logger.warn("Network is not eligible for DNS record registration"); - return; - } - Nic nic = nicDao.findByUuid(nicUuid.asText()); + Nic nic = nicDao.findByUuidIncludingRemoved(nicUuid.asText()); if (nic == null) { logger.error("NIC is not found for the ID: {}", nicUuid); + return; } - - boolean dnsRecordAdded = providerManager.processDnsRecordForInstance(vmInstanceVO, network, nic, isAddDnsRecord); - if (!dnsRecordAdded) { - logger.error("Failure {} DNS record for Instance: {} for Network with ID: {}", - isAddDnsRecord ? "adding" : "removing", vmUuid, networkUuid); + 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(String vmUuid, boolean isAddDnsRecord) { @@ -156,13 +151,24 @@ private void handleVmEvent(String vmUuid, boolean isAddDnsRecord) { List vmNics = nicDao.listByVmId(vmInstanceVO.getId()); for (NicVO nic : vmNics) { Network network = networkDao.findById(nic.getNetworkId()); - if (Network.GuestType.Shared.equals(network.getGuestType())) { - boolean dnsRecordAdded = providerManager.processDnsRecordForInstance(vmInstanceVO, network, nic, isAddDnsRecord); - if (!dnsRecordAdded) { - logger.error("Failure {} DNS record for Instance: {} for Network with ID: {}", - isAddDnsRecord ? "adding" : "removing", vmUuid, network.getUuid()); - } + 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) { + String dnsRecordUrl = providerManager.processDnsRecordForInstance(vmInstanceVO, network, nic, isAddDnsRecord); + if (dnsRecordUrl != null) { + if (isAddDnsRecord) { + nicDetailsDao.addDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD, dnsRecordUrl, true); + } else { + nicDetailsDao.removeDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD); + } + } else { + logger.error("Failure {} DNS record for Instance: {} for Network with ID: {}", + isAddDnsRecord ? "adding" : "removing", vmInstanceVO.getUuid(), network.getUuid()); } } @@ -188,6 +194,8 @@ private boolean isEventCompleted(JsonNode descJson) { if (statusNode == null || statusNode.isNull()) { return false; } + + logger.debug("Processing Event: {}", descJson); return ApiConstants.COMPLETED.equalsIgnoreCase(statusNode.asText()); } } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java index 1d522d0f16b6..29e9190d542c 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java @@ -17,13 +17,11 @@ package org.apache.cloudstack.dns.dao; -import java.util.List; - import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; import com.cloud.utils.db.GenericDao; public interface DnsZoneNetworkMapDao extends GenericDao { DnsZoneNetworkMapVO findByZoneAndNetwork(long dnsZoneId, long networkId); - List listByNetworkId(long networkId); + DnsZoneNetworkMapVO findByNetworkId(long networkId); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java index aec93a500ad5..01a8718a8955 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java @@ -17,8 +17,6 @@ package org.apache.cloudstack.dns.dao; -import java.util.List; - import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; import org.springframework.stereotype.Component; @@ -54,9 +52,9 @@ public DnsZoneNetworkMapVO findByZoneAndNetwork(long dnsZoneId, long networkId) } @Override - public List listByNetworkId(long networkId) { + public DnsZoneNetworkMapVO findByNetworkId(long networkId) { SearchCriteria sc = NetworkSearch.create(); sc.setParameters(ApiConstants.NETWORK_ID, networkId); - return listBy(sc); + return findOneBy(sc); } } diff --git a/ui/src/views/network/NicsTable.vue b/ui/src/views/network/NicsTable.vue index a31925a6b374..e9e15a0aa90c 100644 --- a/ui/src/views/network/NicsTable.vue +++ b/ui/src/views/network/NicsTable.vue @@ -57,6 +57,10 @@ {{ record.isolationuri }} + + + {{ record.dns_url }} + From 856158c1fe2002dadc44082063fef56a97d883c5 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 3 Mar 2026 16:54:57 +0530 Subject: [PATCH 21/23] starting on ui work --- ui/public/locales/en.json | 4 ++++ ui/src/config/section/network.js | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 9987d3528336..0379aaab169d 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -291,6 +291,7 @@ "label.add.internal.lb": "Add internal LB", "label.add.ip.range": "Add IP Range", "label.add.ipv4.subnet": "Add IPv4 Subnet for Routed Networks", +"label.dns.server": "DNS Server", "label.add.ip.v6.prefix": "Add IPv6 prefix", "label.add.isolated.network": "Add Isolated Network", "label.add.kubernetes.cluster": "Add Kubernetes Cluster", @@ -933,6 +934,9 @@ "label.dns": "DNS", "label.dns1": "DNS 1", "label.dns2": "DNS 2", +"label.dns.records": "DNS Records", +"label.dns.zone": "DNS Zone", +"label.dns.zones": "DNS Zones", "label.domain": "Domain", "label.domain.id": "Domain ID", "label.domain.name": "Domain name", diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index 33b39d271726..733679f30fd4 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -1487,6 +1487,47 @@ export default { groupMap: (selection) => { return selection.map(x => { return { id: x } }) } } ] + }, + { + name: 'dnsrecords', + title: 'label.dns.records', + icon: 'global-outlined', + hidden: true, + permission: ['listDnsRecords'], + columns: ['name', 'url', 'provider'], + details: ['name', 'url', 'provider', 'ispublic', 'port', 'nameservers'], + related: [{ + name: 'vm', + title: 'label.dns.zone', + param: 'dnszoneid' + }] + }, + { + name: 'dnszones', + title: 'label.dns.zones', + icon: 'global-outlined', + hidden: true, + permission: ['listDnsZones'], + columns: ['name', 'state', 'dnsservername', 'dnsserveraccount'], + details: ['name', 'state', 'dnsservername', 'dnsserveraccount'], + related: [{ + name: 'dnsrecords', + title: 'label.dns.records', + param: 'dnszoneid' + }] + }, + { + name: 'dnsservers', + title: 'label.dns.server', + icon: 'global-outlined', + permission: ['listDnsServers'], + columns: ['name', 'url', 'provider'], + details: ['name', 'url', 'ispublic', 'port', 'nameservers', 'domain', 'account'], + related: [{ + name: 'dnszones', + title: 'label.dns.zone', + param: 'dnsserverid' + }] } ] } From 369bb16d0dbba98f1a83c70af09e51712ee2e5d3 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Tue, 3 Mar 2026 17:44:24 +0530 Subject: [PATCH 22/23] fix lint issue --- .../cloudstack/api/command/user/dns/ListDnsRecordsCmd.java | 2 +- .../org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java | 3 +-- .../META-INF/cloudstack/core/spring-event-bus-context.xml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java index e1c6b6b42e73..246c8bee9aab 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java @@ -51,4 +51,4 @@ public void execute() { response.setResponseName(getCommandName()); setResponseObject(response); } -} \ No newline at end of file +} diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java index 8c19568bb463..d0a73e48d87b 100644 --- a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java @@ -80,5 +80,4 @@ public void testNormalizeZoneNormalization() { result = client.normalizeRecordName("www", "example.com."); assertEquals("www.example.com.", result); } - -} \ No newline at end of file +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml index 36c26050aaa6..6751e8a34b26 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml @@ -32,4 +32,4 @@ - \ No newline at end of file + From 0df50cedf865b13b7d6e54c28971e6a1fbfad95c Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 4 Mar 2026 19:40:37 +0530 Subject: [PATCH 23/23] fix list dnsservers api, ui screens for dns servers, generate events --- .../main/java/com/cloud/event/EventTypes.java | 2 + .../java/com/cloud/user/AccountService.java | 9 +- .../api/command/user/dns/AddDnsServerCmd.java | 8 +- .../cloudstack/dns/DnsProviderManager.java | 3 - .../java/com/cloud/acl/DomainChecker.java | 6 - .../com/cloud/user/AccountManagerImpl.java | 19 +- .../dns/DnsProviderManagerImpl.java | 102 ++------ .../cloudstack/dns/dao/DnsServerDao.java | 4 + .../cloudstack/dns/dao/DnsServerDaoImpl.java | 43 ++++ ui/public/locales/en.json | 8 + ui/src/components/view/DetailsTab.vue | 7 + ui/src/config/section/network.js | 48 +++- ui/src/views/network/dns/AddDnsServer.vue | 243 ++++++++++++++++++ 13 files changed, 400 insertions(+), 102 deletions(-) create mode 100644 ui/src/views/network/dns/AddDnsServer.vue diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index e4ac32d06ee9..a770cba2a9ee 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -864,8 +864,10 @@ public class EventTypes { // DNS Framework Events public static final String EVENT_DNS_SERVER_ADD = "DNS.SERVER.ADD"; + public static final String EVENT_DNS_SERVER_UPDATE = "DNS.SERVER.UPDATE"; public static final String EVENT_DNS_SERVER_DELETE = "DNS.SERVER.DELETE"; public static final String EVENT_DNS_ZONE_CREATE = "DNS.ZONE.CREATE"; + public static final String EVENT_DNS_ZONE_UPDATE = "DNS.ZONE.UPDATE"; 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"; diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index eb47b75ac5ba..bb33aedcbcdb 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.Map; -import com.cloud.utils.Pair; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -27,6 +26,9 @@ import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.RegisterUserKeyCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.dns.DnsServer; import com.cloud.dc.DataCenter; import com.cloud.domain.Domain; @@ -35,8 +37,7 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; -import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; -import org.apache.cloudstack.backup.BackupOffering; +import com.cloud.utils.Pair; public interface AccountService { @@ -119,6 +120,8 @@ User createUser(String userName, String password, String firstName, String lastN void checkAccess(Account account, BackupOffering bof) throws PermissionDeniedException; + void checkAccess(Account account, DnsServer dnsServer) throws PermissionDeniedException; + void checkAccess(User user, ControlledEntity entity); void checkAccess(Account account, AccessType accessType, boolean sameOwner, String apiName, ControlledEntity... entities) throws PermissionDeniedException; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java index 798e348e2c3e..40464096181b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -55,8 +55,8 @@ public class AddDnsServerCmd extends BaseCmd { @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "API URL of the provider") private String url; - @Parameter(name = ApiConstants.PROVIDER_TYPE, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") - private String providerType; + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") + private String provider; @Parameter(name = ApiConstants.DNS_USER_NAME, type = CommandType.STRING, description = "Username or email associated with the external DNS provider account (used for authentication)") @@ -109,8 +109,8 @@ public List getNameServers() { return nameServers; } - public DnsProviderType getProviderType() { - DnsProviderType dnsProviderType = EnumUtils.getEnumIgnoreCase(DnsProviderType.class, providerType, DnsProviderType.PowerDNS); + public DnsProviderType getProvider() { + DnsProviderType dnsProviderType = EnumUtils.getEnumIgnoreCase(DnsProviderType.class, provider, DnsProviderType.PowerDNS); if (dnsProviderType == null) { throw new InvalidParameterValueException(String.format("Invalid value passed for provider type, valid values are: %s", EnumUtils.listValues(DnsProviderType.values()))); 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 f1af22681ba8..b4dede12ce14 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -38,7 +38,6 @@ 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; @@ -75,7 +74,5 @@ public interface DnsProviderManager extends Manager, PluggableService { boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd); - void checkDnsServerPermissions(Account caller, DnsServer server); - String processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd); } diff --git a/server/src/main/java/com/cloud/acl/DomainChecker.java b/server/src/main/java/com/cloud/acl/DomainChecker.java index b9a017fd4ee3..9c8314dc252b 100644 --- a/server/src/main/java/com/cloud/acl/DomainChecker.java +++ b/server/src/main/java/com/cloud/acl/DomainChecker.java @@ -30,8 +30,6 @@ import org.apache.cloudstack.backup.BackupOffering; import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.dns.DnsProviderManager; -import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; import org.springframework.stereotype.Component; @@ -103,8 +101,6 @@ public class DomainChecker extends AdapterBase implements SecurityChecker { private ProjectDao projectDao; @Inject private AccountService accountService; - @Inject - private DnsProviderManager dnsProviderManager; protected DomainChecker() { super(); @@ -220,8 +216,6 @@ public boolean checkAccess(Account caller, ControlledEntity entity, AccessType a _networkMgr.checkRouterPermissions(caller, (VirtualRouter)entity); } else if (entity instanceof AffinityGroup) { return false; - } else if (entity instanceof DnsServer) { - dnsProviderManager.checkDnsServerPermissions(caller, (DnsServer) entity); } else { validateCallerHasAccessToEntityOwner(caller, entity, accessType); } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 7a0fb8248f75..2f6b64262b85 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -179,9 +179,9 @@ import com.cloud.utils.ConstantTimeComparator; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; import com.cloud.utils.Ternary; import com.cloud.utils.UuidUtils; -import com.cloud.utils.StringUtils; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.Manager; import com.cloud.utils.component.ManagerBase; @@ -737,8 +737,7 @@ public void checkAccess(Account caller, AccessType accessType, boolean sameOwner } if (entity.getAccountId() != -1 && domainId != -1 && !(entity instanceof VirtualMachineTemplate) && !(entity instanceof Network && (accessType == AccessType.UseEntry || accessType == AccessType.OperateEntry)) - && !(entity instanceof AffinityGroup) && !(entity instanceof VirtualRouter) - && !(entity instanceof DnsServer)) { + && !(entity instanceof AffinityGroup) && !(entity instanceof VirtualRouter)) { List toBeChecked = domains.get(entity.getDomainId()); // for templates, we don't have to do cross domains check if (toBeChecked == null) { @@ -3636,6 +3635,20 @@ public void checkAccess(Account account, BackupOffering bof) throws PermissionDe throw new PermissionDeniedException("There's no way to confirm " + account + " has access to " + bof); } + @Override + public void checkAccess(Account caller, DnsServer dnsServer) throws PermissionDeniedException { + if (caller.getId() == dnsServer.getAccountId()) { + return; + } + if (!dnsServer.isPublicServer()) { + throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + dnsServer.getName()); + } + Account owner = getAccount(dnsServer.getAccountId()); + if (!_domainDao.isChildDomain(owner.getDomainId(), caller.getDomainId())) { + throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + dnsServer.getName()); + } + } + @Override public void checkAccess(User user, ControlledEntity entity) throws PermissionDeniedException { for (SecurityChecker checker : _securityCheckers) { 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 a68c7d78fe98..9c8bfe4ba30d 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -60,22 +60,20 @@ import org.springframework.stereotype.Component; import com.cloud.domain.dao.DomainDao; +import com.cloud.event.ActionEvent; +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.projects.Project; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.utils.Pair; import com.cloud.utils.StringUtils; -import com.cloud.utils.Ternary; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; import com.cloud.utils.db.Filter; -import com.cloud.utils.db.SearchBuilder; -import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.vm.Nic; import com.cloud.vm.VirtualMachine; @@ -119,6 +117,7 @@ private DnsProvider getProviderByType(DnsProviderType type) { } @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_SERVER_ADD, eventDescription = "Adding a DNS Server") public DnsServer addDnsServer(AddDnsServerCmd cmd) { Account caller = CallContext.current().getCallingAccount(); DnsServer existing = dnsServerDao.findByUrlAndAccount(cmd.getUrl(), caller.getId()); @@ -139,7 +138,7 @@ public DnsServer addDnsServer(AddDnsServerCmd cmd) { publicDomainSuffix = DnsProviderUtil.normalizeDomain(publicDomainSuffix); } - DnsProviderType type = cmd.getProviderType(); + DnsProviderType type = cmd.getProvider(); DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), cmd.getPort(), cmd.getExternalServerId(), type, cmd.getDnsUserName(), cmd.getCredentials(), isDnsPublic, publicDomainSuffix, cmd.getNameServers(), caller.getAccountId(), caller.getDomainId()); @@ -159,12 +158,15 @@ public DnsServer addDnsServer(AddDnsServerCmd cmd) { @Override public ListResponse listDnsServers(ListDnsServersCmd cmd) { Pair, Integer> result = searchForDnsServerInternal(cmd); + ListResponse response = new ListResponse<>(); + if (result == null) { + return response; + } List serverIds = new ArrayList<>(); for (DnsServer server : result.first()) { serverIds.add(server.getUuid()); } List joinResult = dnsServerJoinDao.listByUuids(serverIds); - ListResponse response = new ListResponse<>(); List serverResponses = new ArrayList<>(); for (DnsServerJoinVO server : joinResult) { serverResponses.add(createDnsServerResponse(server)); @@ -176,64 +178,20 @@ public ListResponse listDnsServers(ListDnsServersCmd cmd) { private Pair, Integer> searchForDnsServerInternal(ListDnsServersCmd cmd) { Long dnsServerId = cmd.getId(); Account caller = CallContext.current().getCallingAccount(); - Long domainId = cmd.getDomainId(); - boolean isRecursive = cmd.isRecursive(); - - // Step 1: Build ACL search parameters based on caller permissions - List permittedAccountIds = new ArrayList<>(); - Ternary domainIdRecursiveListProject = new Ternary<>( - domainId, isRecursive, null); - accountMgr.buildACLSearchParameters(caller, dnsServerId, cmd.getAccountName(), null, permittedAccountIds, - domainIdRecursiveListProject, cmd.listAll(), false); - - domainId = domainIdRecursiveListProject.first(); - isRecursive = domainIdRecursiveListProject.second(); - Project.ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); - Filter searchFilter = new Filter(DnsServerVO.class, ApiConstants.ID, true, cmd.getStartIndex(), cmd.getPageSizeVal()); - - // Step 2: Search for caller's own DNS servers using standard ACL pattern - SearchBuilder sb = dnsServerDao.createSearchBuilder(); - accountMgr.buildACLSearchBuilder(sb, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); - sb.and(ApiConstants.STATE, sb.entity().getState(), SearchCriteria.Op.EQ); - sb.and(ApiConstants.PROVIDER_TYPE, sb.entity().getProviderType(), SearchCriteria.Op.EQ); - sb.done(); - - SearchCriteria sc = sb.create(); - accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccountIds, listProjectResourcesCriteria); - sc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled); - sc.setParameters(ApiConstants.PROVIDER_TYPE, cmd.getProviderType()); - - Pair, Integer> ownServersPair = dnsServerDao.searchAndCount(sc, searchFilter); - List dnsServers = new ArrayList<>(ownServersPair.first()); - int count = ownServersPair.second(); - if (cmd.getId() == null) { - Set parentDomainIds = domainDao.getDomainParentIds(caller.getDomainId()); - if (!parentDomainIds.isEmpty()) { - SearchBuilder publicSb = dnsServerDao.createSearchBuilder(); - publicSb.and(ApiConstants.IS_PUBLIC, publicSb.entity().isPublicServer(), SearchCriteria.Op.EQ); - publicSb.and(ApiConstants.DOMAIN_IDS, publicSb.entity().getDomainId(), SearchCriteria.Op.IN); - publicSb.and(ApiConstants.STATE, publicSb.entity().getState(), SearchCriteria.Op.EQ); - publicSb.and(ApiConstants.PROVIDER_TYPE, publicSb.entity().getProviderType(), SearchCriteria.Op.EQ); - publicSb.done(); - SearchCriteria publicSc = publicSb.create(); - publicSc.setParameters(ApiConstants.IS_PUBLIC, 1); - publicSc.setParameters(ApiConstants.DOMAIN_IDS, parentDomainIds.toArray()); - publicSc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled); - publicSc.setParameters(ApiConstants.PROVIDER_TYPE, cmd.getProviderType()); - List publicServers = dnsServerDao.search(publicSc, null); - List ownServerIds = dnsServers.stream().map(DnsServerVO::getId).collect(Collectors.toList()); - for (DnsServerVO publicServer : publicServers) { - if (!ownServerIds.contains(publicServer.getId())) { - dnsServers.add(publicServer); - count++; - } - } + if (dnsServerId != null) { + DnsServerVO dnsServerVO = dnsServerDao.findById(dnsServerId); + if (dnsServerVO == null) { + return null; } + return new Pair<>(Collections.singletonList(dnsServerVO), 1); } - return new Pair<>(dnsServers, count); + Set parentDomainIds = domainDao.getDomainParentIds(caller.getDomainId()); + Filter searchFilter = new Filter(DnsServerVO.class, ApiConstants.ID, true, cmd.getStartIndex(), cmd.getPageSizeVal()); + return dnsServerDao.searchDnsServer(dnsServerId, caller.getAccountId(), parentDomainIds, cmd.getProviderType(), cmd.getKeyword(), searchFilter); } @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_SERVER_UPDATE, eventDescription = "Updating DNS Server") public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { Long dnsServerId = cmd.getId(); DnsServerVO dnsServer = dnsServerDao.findById(dnsServerId); @@ -242,7 +200,7 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { } Account caller = CallContext.current().getCallingAccount(); - accountMgr.checkAccess(caller, null, true, dnsServer); + accountMgr.checkAccess(caller, dnsServer); boolean validationRequired = false; String originalUrl = dnsServer.getUrl(); @@ -305,6 +263,7 @@ public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { } @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_SERVER_DELETE, eventDescription = "Deleting DNS Server") public boolean deleteDnsServer(DeleteDnsServerCmd cmd) { Long dnsServerId = cmd.getId(); DnsServerVO dnsServer = dnsServerDao.findById(dnsServerId); @@ -312,11 +271,12 @@ public boolean deleteDnsServer(DeleteDnsServerCmd cmd) { throw new InvalidParameterValueException(String.format("DNS server with ID: %s not found.", dnsServerId)); } Account caller = CallContext.current().getCallingAccount(); - accountMgr.checkAccess(caller, null, true, dnsServer); + accountMgr.checkAccess(caller, dnsServer); return dnsServerDao.remove(dnsServerId); } @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_ZONE_DELETE, eventDescription = "Deleting DNS Zone") public boolean deleteDnsZone(Long zoneId) { DnsZoneVO zone = dnsZoneDao.findById(zoneId); if (zone == null) { @@ -340,6 +300,7 @@ public boolean deleteDnsZone(Long zoneId) { } @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_ZONE_UPDATE, eventDescription = "Updating DNS Zone") public DnsZone updateDnsZone(UpdateDnsZoneCmd cmd) { DnsZoneVO dnsZone = dnsZoneDao.findById(cmd.getId()); if (dnsZone == null) { @@ -396,7 +357,7 @@ private Pair, Integer> searchForDnsZonesInternal(ListDnsZonesCmd Account caller = CallContext.current().getCallingAccount(); if (cmd.getDnsServerId() != null) { DnsServer dnsServer = dnsServerDao.findById(cmd.getDnsServerId()); - accountMgr.checkAccess(caller, null, false, dnsServer); + accountMgr.checkAccess(caller, dnsServer); } List ownDnsServerIds = dnsServerDao.listDnsServerIdsByAccountId(caller.getAccountId()); String keyword = cmd.getKeyword(); @@ -408,6 +369,7 @@ private Pair, Integer> searchForDnsZonesInternal(ListDnsZonesCmd } @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_RECORD_CREATE, eventDescription = "Creating DNS Record") public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { String recordName = StringUtils.trimToEmpty(cmd.getName()).toLowerCase(); if (StringUtils.isBlank(recordName)) { @@ -436,6 +398,7 @@ public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { } @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) { @@ -651,21 +614,6 @@ public boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd return dnsZoneNetworkMapDao.remove(mapping.getId()); } - - @Override - public void checkDnsServerPermissions(Account caller, DnsServer server) { - if (caller.getId() == server.getAccountId()) { - return; - } - if (!server.isPublicServer()) { - throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + server.getName()); - } - Account owner = accountMgr.getAccount(server.getAccountId()); - if (!domainDao.isChildDomain(caller.getDomainId(), owner.getDomainId())) { - throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + server.getName()); - } - } - @Override public String processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd) { long networkId = network.getId(); diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java index c9cd68c05130..8a884620c4b6 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java @@ -18,7 +18,9 @@ package org.apache.cloudstack.dns.dao; import java.util.List; +import java.util.Set; +import org.apache.cloudstack.dns.DnsProviderType; import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.dns.vo.DnsServerVO; @@ -32,4 +34,6 @@ public interface DnsServerDao extends GenericDao { List listDnsServerIdsByAccountId(Long accountId); Pair, Integer> searchDnsServers(Long id, String keyword, String provider, Long accountId, Filter filter); + + Pair, Integer> searchDnsServer(Long dnsServerId, Long accountId, Set domainIds, DnsProviderType providerType, String keyword, Filter filter); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java index 655cdf9a3e80..095b4eb640c2 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java @@ -18,10 +18,13 @@ package org.apache.cloudstack.dns.dao; import java.util.List; +import java.util.Set; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.DnsProviderType; import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.dns.vo.DnsServerVO; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import com.cloud.utils.Pair; @@ -96,4 +99,44 @@ public Pair, Integer> searchDnsServers(Long id, String keyword } return searchAndCount(sc, filter); } + + @Override + public Pair, Integer> searchDnsServer(Long dnsServerId, Long accountId, Set domainIds, DnsProviderType providerType, + String keyword, Filter filter) { + + SearchBuilder sb = createSearchBuilder(); + sb.and(ApiConstants.ID, sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.NAME, sb.entity().getName(), SearchCriteria.Op.LIKE); + + sb.and().op(ApiConstants.ACCOUNT_ID, sb.entity().getAccountId(), SearchCriteria.Op.EQ); + if (!CollectionUtils.isEmpty(domainIds)) { + sb.or().op(ApiConstants.IS_PUBLIC, sb.entity().isPublicServer(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.DOMAIN_IDS, sb.entity().getDomainId(), SearchCriteria.Op.IN); + sb.cp(); + } + sb.cp(); + sb.and(ApiConstants.PROVIDER_TYPE, sb.entity().getProviderType(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.STATE, sb.entity().getState(), SearchCriteria.Op.EQ); + sb.done(); + + SearchCriteria sc = sb.create(); + if (dnsServerId != null) { + sc.setParameters(ApiConstants.ID, dnsServerId); + } + if (accountId != null) { + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + } + if (!CollectionUtils.isEmpty(domainIds)) { + sc.setParameters(ApiConstants.IS_PUBLIC, true); + sc.setParameters(ApiConstants.DOMAIN_IDS, domainIds.toArray()); + } + if (providerType != null) { + sc.setParameters(ApiConstants.PROVIDER_TYPE, providerType); + } + if (keyword != null) { + sc.setParameters(ApiConstants.NAME, "%" + keyword + "%"); + } + sc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled); + return searchAndCount(sc, filter); + } } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index f09fe22ea7cb..e82eb80e7d97 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -91,6 +91,7 @@ "label.action.delete.account": "Delete Account", "label.action.delete.backup.offering": "Delete backup offering", "label.action.delete.cluster": "Delete Cluster", +"label.action.delete.dns.server": "Delete DNS Server", "label.action.delete.domain": "Delete Domain", "label.action.delete.egress.firewall": "Delete Egress Firewall Rule", "label.action.delete.firewall": "Delete Firewall Rule", @@ -292,6 +293,10 @@ "label.add.ip.range": "Add IP Range", "label.add.ipv4.subnet": "Add IPv4 Subnet for Routed Networks", "label.dns.server": "DNS Server", +"label.dns.add.server": "Add DNS Server", +"label.dns.update.server": "Update DNS Server", +"label.dns.delete.server": "Delete DNS Server", +"label.dnsrecords": "DNS Records", "label.add.ip.v6.prefix": "Add IPv6 prefix", "label.add.isolated.network": "Add Isolated Network", "label.add.kubernetes.cluster": "Add Kubernetes Cluster", @@ -713,6 +718,8 @@ "label.created": "Created", "label.creating": "Creating", "label.creating.iprange": "Creating IP ranges", +"label.dns.credentials": "DNS API key", +"label.nameservers": "DNS Nameservers", "label.credit": "Credit", "label.cron": "Cron expression", "label.cron.mode": "Cron mode", @@ -2947,6 +2954,7 @@ "message.action.delete.backup.schedule": "Please confirm that you want to delete this backup schedule?", "message.action.delete.cluster": "Please confirm that you want to delete this Cluster.", "message.action.delete.custom.action": "Please confirm that you want to delete this custom action.", +"message.action.delete.dns.server": "Please confirm you want to delete this DNS server.", "message.action.delete.domain": "Please confirm that you want to delete this domain.", "message.action.delete.extension": "Please confirm that you want to delete the extension", "message.action.delete.external.firewall": "Please confirm that you would like to remove this external firewall. Warning: If you are planning to add back the same external firewall, you must reset usage data on the device.", diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 4145eeb9be6d..37b06809b61c 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -207,6 +207,13 @@ + +
+ {{ $t('label.provider') }} +
+
{{ dataResource[item] }}
+
+
diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index 733679f30fd4..62f5ae199554 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -1510,24 +1510,60 @@ export default { permission: ['listDnsZones'], columns: ['name', 'state', 'dnsservername', 'dnsserveraccount'], details: ['name', 'state', 'dnsservername', 'dnsserveraccount'], - related: [{ + tabs: [{ + name: 'details', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, + { name: 'dnsrecords', - title: 'label.dns.records', - param: 'dnszoneid' + component: shallowRef(defineAsyncComponent(() => import('@/views/network/InternalLBAssignedVmTab.vue'))), + show: () => true }] }, { - name: 'dnsservers', + name: 'dnsserver', title: 'label.dns.server', icon: 'global-outlined', permission: ['listDnsServers'], columns: ['name', 'url', 'provider'], - details: ['name', 'url', 'ispublic', 'port', 'nameservers', 'domain', 'account'], + details: ['name', 'url', 'abc', 'provider', 'ispublic', 'port', 'nameservers', 'domain', 'account'], related: [{ name: 'dnszones', title: 'label.dns.zone', param: 'dnsserverid' - }] + }], + actions: [ + { + api: 'addDnsServer', + icon: 'plus-outlined', + label: 'label.dns.add.server', + listView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/network/dns/AddDnsServer.vue'))), + show: () => { + return true + } + }, + { + api: 'updateDnsServer', + icon: 'edit-outlined', + label: 'label.dns.update.server', + dataView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/network/dns/AddDnsServer.vue'))), + show: (record) => { return true } + }, + { + api: 'deleteDnsServer', + icon: 'delete-outlined', + label: 'label.dns.delete.server', + message: 'message.action.delete.dns.server', + dataView: true, + groupAction: true, + show: (record) => { return true }, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) } + } + ] } ] } diff --git a/ui/src/views/network/dns/AddDnsServer.vue b/ui/src/views/network/dns/AddDnsServer.vue new file mode 100644 index 000000000000..404870f9d829 --- /dev/null +++ b/ui/src/views/network/dns/AddDnsServer.vue @@ -0,0 +1,243 @@ +// 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. + + + + + +