You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by we...@apache.org on 2026/07/07 07:39:21 UTC
(cloudstack) branch main updated: CloudStack DNS framework - Integrate PowerDNS as first plugin (#12737)
This is an automated email from the ASF dual-hosted git repository.
weizhouapache pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push:
new 167eb0af759 CloudStack DNS framework - Integrate PowerDNS as first plugin (#12737)
167eb0af759 is described below
commit 167eb0af75950ecd054b62c8ee51a3b2f1495006
Author: Manoj Kumar <ma...@gmail.com>
AuthorDate: Tue Jul 7 13:09:15 2026 +0530
CloudStack DNS framework - Integrate PowerDNS as first plugin (#12737)
---
api/src/main/java/com/cloud/event/EventTypes.java | 25 +
api/src/main/java/com/cloud/vm/Nic.java | 5 +
.../org/apache/cloudstack/api/ApiConstants.java | 32 +
.../java/org/apache/cloudstack/api/BaseCmd.java | 4 +
.../api/command/user/dns/AddDnsServerCmd.java | 169 +++
.../user/dns/AssociateDnsZoneToNetworkCmd.java | 94 ++
.../api/command/user/dns/CreateDnsRecordCmd.java | 100 ++
.../api/command/user/dns/CreateDnsZoneCmd.java | 154 ++
.../api/command/user/dns/DeleteDnsRecordCmd.java | 92 ++
.../api/command/user/dns/DeleteDnsServerCmd.java | 115 ++
.../api/command/user/dns/DeleteDnsZoneCmd.java | 109 ++
.../dns/DisassociateDnsZoneFromNetworkCmd.java | 80 ++
.../api/command/user/dns/ListDnsProvidersCmd.java | 53 +
.../api/command/user/dns/ListDnsRecordsCmd.java | 54 +
.../api/command/user/dns/ListDnsServersCmd.java | 82 ++
.../api/command/user/dns/ListDnsZonesCmd.java | 63 +
.../api/command/user/dns/UpdateDnsServerCmd.java | 150 ++
.../api/command/user/dns/UpdateDnsZoneCmd.java | 95 ++
.../api/response/DnsProviderResponse.java | 30 +-
.../cloudstack/api/response/DnsRecordResponse.java | 57 +
.../cloudstack/api/response/DnsServerResponse.java | 133 ++
.../api/response/DnsZoneNetworkMapResponse.java | 64 +
.../cloudstack/api/response/DnsZoneResponse.java | 139 ++
.../cloudstack/api/response/NetworkResponse.java | 16 +
.../cloudstack/api/response/NicResponse.java | 8 +
.../org/apache/cloudstack/dns/DnsProvider.java | 45 +
.../apache/cloudstack/dns/DnsProviderManager.java | 78 +
.../org/apache/cloudstack/dns/DnsProviderType.java | 10 +-
.../java/org/apache/cloudstack/dns/DnsRecord.java | 66 +
.../java/org/apache/cloudstack/dns/DnsServer.java | 62 +
.../java/org/apache/cloudstack/dns/DnsZone.java | 34 +-
.../dns/exception/DnsAuthenticationException.java | 14 +-
.../dns/exception/DnsConflictException.java | 14 +-
.../dns/exception/DnsNotFoundException.java | 14 +-
.../dns/exception/DnsOperationException.java | 14 +-
.../dns/exception/DnsProviderException.java | 13 +-
.../dns/exception/DnsTransportException.java | 15 +-
.../api/command/user/dns/AddDnsServerCmdTest.java | 134 ++
.../user/dns/AssociateDnsZoneToNetworkCmdTest.java | 97 ++
.../api/command/user/dns/BaseDnsCmdTest.java | 88 ++
.../command/user/dns/CreateDnsRecordCmdTest.java | 117 ++
.../api/command/user/dns/CreateDnsZoneCmdTest.java | 157 ++
.../command/user/dns/DeleteDnsRecordCmdTest.java | 102 ++
.../command/user/dns/DeleteDnsServerCmdTest.java | 119 ++
.../api/command/user/dns/DeleteDnsZoneCmdTest.java | 104 ++
.../dns/DisassociateDnsZoneFromNetworkCmdTest.java | 100 ++
.../command/user/dns/ListDnsProvidersCmdTest.java | 83 ++
.../command/user/dns/ListDnsRecordsCmdTest.java | 56 +
.../command/user/dns/ListDnsServersCmdTest.java | 67 +
.../api/command/user/dns/ListDnsZonesCmdTest.java | 60 +
.../command/user/dns/UpdateDnsServerCmdTest.java | 145 ++
.../api/command/user/dns/UpdateDnsZoneCmdTest.java | 96 ++
.../org/apache/cloudstack/dns/DnsRecordTest.java | 105 ++
client/pom.xml | 6 +
.../core/spring-core-registry-core-context.xml | 2 +
.../META-INF/cloudstack/dns/module.properties | 21 +
...ring-core-lifecycle-dns-context-inheritable.xml | 31 +
.../java/com/cloud/vm/VirtualMachineManager.java | 2 +
.../main/java/com/cloud/vm/dao/NicDetailsDao.java | 4 +
.../java/com/cloud/vm/dao/NicDetailsDaoImpl.java | 28 +-
.../spring-engine-schema-core-daos-context.xml | 8 +
.../resources/META-INF/db/schema-42210to42300.sql | 78 +
.../META-INF/db/views/cloud.dns_server_view.sql | 44 +
.../META-INF/db/views/cloud.dns_zone_view.sql | 45 +
.../META-INF/db/views/cloud.user_vm_view.sql | 4 +-
.../resources/META-INF/db/views/nic_dns_view.sql | 40 +
plugins/dns/powerdns/pom.xml | 30 +
.../cloudstack/dns/powerdns/PowerDnsClient.java | 390 +++++
.../cloudstack/dns/powerdns/PowerDnsProvider.java | 202 +++
.../META-INF/cloudstack/powerdns/module.properties | 18 +
.../powerdns/spring-dns-powerdns-context.xml | 31 +
.../apache/cloudstack/dns/DnsProviderUtilTest.java | 101 ++
.../dns/NormalizeDnsRecordValueTest.java | 197 +++
.../dns/powerdns/PowerDnsClientTest.java | 432 ++++++
.../dns/powerdns/PowerDnsProviderTest.java | 391 +++++
plugins/pom.xml | 1 +
.../src/main/java/com/cloud/acl/DomainChecker.java | 11 +-
server/src/main/java/com/cloud/api/ApiDBUtils.java | 24 +
.../main/java/com/cloud/api/ApiResponseHelper.java | 9 +
.../com/cloud/api/query/dao/UserVmJoinDaoImpl.java | 4 +
.../java/com/cloud/api/query/vo/UserVmJoinVO.java | 12 +-
.../network/firewall/FirewallManagerImpl.java | 2 +-
.../java/com/cloud/user/AccountManagerImpl.java | 5 +-
.../main/java/com/cloud/vm/UserVmManagerImpl.java | 71 +-
.../cloudstack/dns/DnsProviderManagerImpl.java | 1230 ++++++++++++++++
.../org/apache/cloudstack/dns/DnsProviderUtil.java | 249 ++++
.../apache/cloudstack/dns/dao/DnsServerDao.java | 27 +-
.../cloudstack/dns/dao/DnsServerDaoImpl.java | 196 +++
.../cloudstack/dns/dao/DnsServerDetailsDao.java | 7 +-
.../dns/dao/DnsServerDetailsDaoImpl.java | 12 +-
.../cloudstack/dns/dao/DnsServerJoinDao.java | 9 +-
.../cloudstack/dns/dao/DnsServerJoinDaoImpl.java | 10 +-
.../org/apache/cloudstack/dns/dao/DnsZoneDao.java | 20 +-
.../apache/cloudstack/dns/dao/DnsZoneDaoImpl.java | 128 ++
.../apache/cloudstack/dns/dao/DnsZoneJoinDao.java | 9 +-
.../cloudstack/dns/dao/DnsZoneJoinDaoImpl.java | 10 +-
.../cloudstack/dns/dao/DnsZoneNetworkMapDao.java | 12 +-
.../dns/dao/DnsZoneNetworkMapDaoImpl.java | 64 +
.../apache/cloudstack/dns/dao/NicDnsJoinDao.java | 67 +
.../cloudstack/dns/dao/NicDnsJoinDaoImpl.java | 107 ++
.../cloudstack/dns/vo/DnsServerDetailVO.java | 99 ++
.../apache/cloudstack/dns/vo/DnsServerJoinVO.java | 154 ++
.../org/apache/cloudstack/dns/vo/DnsServerVO.java | 271 ++++
.../apache/cloudstack/dns/vo/DnsZoneJoinVO.java | 130 ++
.../cloudstack/dns/vo/DnsZoneNetworkMapVO.java | 105 ++
.../org/apache/cloudstack/dns/vo/DnsZoneVO.java | 164 +++
.../org/apache/cloudstack/dns/vo/NicDnsJoinVO.java | 115 ++
.../core/spring-server-core-managers-context.xml | 4 +
.../cloudstack/dns/DnsProviderManagerImplTest.java | 1518 ++++++++++++++++++++
.../cloudstack/dns/dao/DnsServerDaoImplTest.java | 117 ++
.../cloudstack/dns/dao/DnsZoneDaoImplTest.java | 111 ++
.../dns/dao/DnsZoneNetworkMapDaoImplTest.java | 91 ++
.../cloudstack/dns/vo/DnsServerJoinVOTest.java | 100 ++
.../apache/cloudstack/dns/vo/DnsServerVOTest.java | 144 ++
.../cloudstack/dns/vo/DnsZoneJoinVOTest.java | 82 ++
.../smoke/test_dns_framework_powerdns.py | 401 ++++++
tools/apidoc/gen_toc.py | 3 +-
ui/public/locales/en.json | 57 +
ui/src/components/view/DetailsTab.vue | 7 +
ui/src/components/view/ListView.vue | 5 +-
ui/src/config/section/network.js | 134 +-
ui/src/views/network/NicsTable.vue | 3 +
ui/src/views/network/dns/AddDnsServer.vue | 381 +++++
ui/src/views/network/dns/AssociateDnsZone.vue | 194 +++
ui/src/views/network/dns/CreateDnsRecord.vue | 207 +++
ui/src/views/network/dns/CreateDnsZone.vue | 222 +++
ui/src/views/network/dns/DeleteDnsServer.vue | 234 +++
ui/src/views/network/dns/DeleteDnsZone.vue | 184 +++
ui/src/views/network/dns/DnsRecordsTab.vue | 262 ++++
ui/src/views/network/dns/UpdateDnsServer.vue | 320 +++++
ui/src/views/network/dns/UpdateDnsZone.vue | 152 ++
131 files changed, 14188 insertions(+), 116 deletions(-)
diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java
index 23e79173bdf..c97e916fe43 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.api.response.ZoneResponse;
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;
@@ -883,6 +886,17 @@ 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_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";
+ public static final String EVENT_DNS_NAME_COLLISION = "DNS.NAME.COLLISION";
+
static {
// TODO: need a way to force author adding event types to declare the entity details as well, with out braking
@@ -1441,6 +1455,17 @@ 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_UPDATE, DnsServer.class);
+ entityEventDetails.put(EVENT_DNS_SERVER_DELETE, DnsServer.class);
+ entityEventDetails.put(EVENT_DNS_ZONE_CREATE, DnsZone.class);
+ entityEventDetails.put(EVENT_DNS_ZONE_UPDATE, 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/com/cloud/vm/Nic.java b/api/src/main/java/com/cloud/vm/Nic.java
index cc0b294205c..3722e5769c9 100644
--- a/api/src/main/java/com/cloud/vm/Nic.java
+++ b/api/src/main/java/com/cloud/vm/Nic.java
@@ -33,6 +33,11 @@ import com.cloud.utils.fsm.StateMachine;
* Nic represents one nic on the VM.
*/
public interface Nic extends Identity, InternalIdentity {
+
+ interface Topics {
+ String NIC_LIFECYCLE = "nic.lifecycle";
+ }
+
enum Event {
ReservationRequested, ReleaseRequested, CancelRequested, OperationCompleted, OperationFailed,
}
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 15025621a33..82e2f38a522 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -1386,6 +1386,38 @@ public class ApiConstants {
" a VR, CloudStack will use the same MAC address for the public NIC of all VRs. Otherwise, if \"false\", new public NICs will always have " +
" a new MAC address.";
+ // DNS provider related
+ public static final String NAME_SERVERS = "nameservers";
+ public static final String DNS_USER_NAME = "dnsusername";
+ public static final String DNS_API_KEY = "dnsapikey";
+ public static final String DNS_ZONE_ID = "dnszoneid";
+ public static final String DNS_ZONE = "dnszone";
+ public static final String DNS_RECORD = "dnsrecord";
+ public static final String DNS_SUB_DOMAIN = "dnssubdomain";
+ public static final String DNS_SERVER_ID = "dnsserverid";
+ public static final String CONTENT = "content";
+ 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 NATIVE_ZONE = "Native";
+ public static final String NIC_DNS_NAME = "nicdnsname";
+ public static final String TIME_STAMP = "timestamp";
+ public static final String INSTANCE_ID = "instanceId";
+ public static final String OLD_STATE = "oldState";
+ public static final String NEW_STATE = "newState";
+ public static final String OLD_HOST_NAME = "oldHostName";
+ public static final String EXISTING = "existing";
+ public static final String UNMANAGE = "unmanage";
+
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/BaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java
index 00b1bc310d5..483fa83630a 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.affinity.AffinityGroupService;
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;
@@ -232,6 +233,9 @@ public abstract class BaseCmd {
@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 00000000000..298ddd64a31
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java
@@ -0,0 +1,169 @@
+// 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.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+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.DnsServerResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.dns.DnsProviderType;
+import org.apache.cloudstack.dns.DnsServer;
+import org.apache.commons.collections.MapUtils;
+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,
+ entityType = {DnsServer.class},
+ requestHasSensitiveInfo = true,
+ responseHasSensitiveInfo = false,
+ since = "4.23.0",
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class AddDnsServerCmd extends BaseCmd {
+
+ /////////////////////////////////////////////////////
+ //////////////// 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 = 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 DNS provider account (used for authentication)")
+ private String dnsUserName;
+
+ @Parameter(name = ApiConstants.DNS_API_KEY, required = true, type = CommandType.STRING, description = "API key or token for the DNS provider")
+ private String dnsApiKey;
+
+ @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 this DNS server can be used by accounts other than the owner to create and manage DNS zones")
+ private Boolean isPublic;
+
+ @Parameter(name = ApiConstants.PUBLIC_DOMAIN_SUFFIX, type = CommandType.STRING,
+ description = "Domain suffix that restricts DNS zones created by non-owner accounts to subdomains of this " +
+ "suffix (for example, sub.example.com under example.com)")
+ private String publicDomainSuffix;
+
+ @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.LIST, collectionType = CommandType.STRING,
+ required = true,
+ description = "Comma separated list of name servers; used to create NS records for the DNS Zone (for example, ns1.example.com, ns2.example.com)")
+ private List<String> nameServers;
+
+ @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "Details in key/value pairs using " +
+ "format details[i].keyname=keyvalue. Example: details[0].pdnsServerId=localhost")
+ protected Map details;
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ public String getName() { return name; }
+
+ public String getUrl() { return url; }
+
+ public String getDnsApiKey() {
+ return dnsApiKey;
+ }
+
+ public Integer getPort() {
+ return port;
+ }
+
+ public Boolean isPublic() {
+ return BooleanUtils.isTrue(isPublic);
+ }
+
+ public String getPublicDomainSuffix() {
+ return publicDomainSuffix;
+ }
+
+ public List<String> getNameServers() {
+ return nameServers;
+ }
+
+ 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())));
+ }
+ return dnsProviderType;
+ }
+
+ @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());
+ }
+ }
+
+ public String getDnsUserName() {
+ return dnsUserName;
+ }
+
+ public Map<String, String> getDetails() {
+ Map<String, String> detailsMap = new HashMap<>();
+ if (MapUtils.isNotEmpty(details)) {
+ Collection<?> props = details.values();
+ for (Object prop : props) {
+ HashMap<String, String> detail = (HashMap<String, String>) prop;
+ for (Map.Entry<String, String> entry: detail.entrySet()) {
+ detailsMap.put(entry.getKey(),entry.getValue());
+ }
+ }
+ }
+ return detailsMap;
+ }
+}
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 00000000000..5ffffc7c304
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java
@@ -0,0 +1,94 @@
+// 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.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;
+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;
+import com.cloud.network.Network;
+import com.cloud.user.Account;
+
+@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,
+ 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;
+
+ @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() {
+ Network network = _entityMgr.findById(Network.class, networkId);
+ if (network != null) {
+ return network.getAccountId();
+ }
+ return Account.ACCOUNT_ID_SYSTEM;
+ }
+
+ 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/CreateDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java
new file mode 100644
index 00000000000..d2fe3a5024a
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java
@@ -0,0 +1,100 @@
+// 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;
+
+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.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 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,
+ 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;
+
+ @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "DNS record name")
+ private String name;
+
+ @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "DNS record type (e.g., A, AAAA, CNAME, MX, TXT, etc.)")
+ private String type;
+
+ @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<String> contents;
+
+ @Parameter(name = "ttl", type = CommandType.INTEGER, description = "Time to live")
+ private Integer ttl;
+
+ // Getters
+ public Long getDnsZoneId() { return dnsZoneId; }
+ public String getName() { return name; }
+
+ public List<String> 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 {
+ DnsRecordResponse response = dnsProviderManager.createDnsRecord(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ } catch (Exception e) {
+ throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, 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 00000000000..10a706e3a81
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.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.api.command.user.dns;
+
+import java.util.Arrays;
+
+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.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.context.CallContext;
+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,
+ 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 {
+
+ /////////////////////////////////////////////////////
+ //////////////// 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;
+
+ @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;
+
+ @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING,
+ description = "The type of zone (Public, Private). Defaults to Public.")
+ private String type;
+
+ @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the DNS zone")
+ private String description;
+
+ @Parameter(name = ApiConstants.EXISTING, type = CommandType.BOOLEAN, entityType = DnsZoneResponse.class,
+ description = "If true, imports an existing DNS zone from the DNS provider into CloudStack. " +
+ "If false, creates the zone in the DNS provider and registers it in CloudStack. Default is false")
+ private Boolean existing = false;
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ public String getName() {
+ return name;
+ }
+
+ public Long getDnsServerId() {
+ return dnsServerId;
+ }
+
+ public DnsZone.ZoneType getType() {
+ if (StringUtils.isBlank(type)) {
+ return DnsZone.ZoneType.Public;
+ }
+ DnsZone.ZoneType zoneType = EnumUtils.getEnumIgnoreCase(DnsZone.ZoneType.class, type);
+ if (zoneType == null) {
+ throw new IllegalArgumentException("Invalid type value, supported values are: " + Arrays.toString(DnsZone.ZoneType.values()));
+ }
+ return zoneType;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ /////////////////////////////////////////////////////
+ /////////////// Implementation //////////////////////
+ /////////////////////////////////////////////////////
+
+ @Override
+ public void create() throws ResourceAllocationException {
+ try {
+ DnsZone zone = dnsProviderManager.allocateDnsZone(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() {
+ try {
+ DnsZone result = dnsProviderManager.provisionDnsZone(getEntityId(), isExistingZone());
+ 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 long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getId();
+ }
+
+ @Override
+ public String getEventType() {
+ return EventTypes.EVENT_DNS_ZONE_CREATE;
+ }
+
+ @Override
+ public String getEventDescription() {
+ return "creating DNS zone: " + getName();
+ }
+
+ public Boolean isExistingZone() {
+ return Boolean.TRUE.equals(existing);
+ }
+}
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 00000000000..1d7914c346e
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java
@@ -0,0 +1,92 @@
+// 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.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;
+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 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,
+ 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;
+
+ @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 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; }
+
+ @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 00000000000..099fc62f354
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java
@@ -0,0 +1,115 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.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;
+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.DnsZoneResponse;
+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,
+ 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;
+
+ @Parameter(name = ApiConstants.CLEANUP, type = CommandType.BOOLEAN,
+ entityType = DnsZoneResponse.class, description = "If true, all associated DNS zones will be cleaned up " +
+ "when the server is removed. Default: true")
+ private Boolean cleanup = true;
+
+ @Parameter(name = ApiConstants.UNMANAGE, type = CommandType.BOOLEAN, entityType = DnsZoneResponse.class,
+ description = "If true, the DNS zone is only removed from CloudStack (unmanaged); if false, it is removed " +
+ "from both CloudStack and the DNS provider. Default: false")
+ private Boolean unmanage = false;
+
+ /////////////////////////////////////////////////////
+ /////////////////// 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 long getEntityOwnerId() {
+ 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
+ public String getEventType() { return EventTypes.EVENT_DNS_SERVER_DELETE; }
+
+ @Override
+ public String getEventDescription() { return "Deleting DNS server ID: " + getId(); }
+
+ public Boolean getCleanup() {
+ return Boolean.TRUE.equals(cleanup);
+ }
+
+ public Boolean isUnmanage() {
+ return Boolean.TRUE.equals(unmanage);
+ }
+}
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 00000000000..a6d5a710ae5
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java
@@ -0,0 +1,109 @@
+// 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.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;
+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,
+ 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 /////////////////////
+ /////////////////////////////////////////////////////
+
+ @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;
+
+ @Parameter(name = ApiConstants.UNMANAGE, type = CommandType.BOOLEAN, entityType = DnsZoneResponse.class,
+ description = "If true, removes the DNS zone only from CloudStack; if false, removes it from " +
+ "both CloudStack and the DNS provider. Default: false")
+ private Boolean unmanage = false;
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ public Long getId() {
+ return id;
+ }
+
+ /////////////////////////////////////////////////////
+ /////////////// API Implementation //////////////////////
+ /////////////////////////////////////////////////////
+
+ @Override
+ public void execute() {
+ try {
+ boolean result = dnsProviderManager.deleteDnsZone(getId(), isUnmanage());
+ 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, e.getMessage());
+ }
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ DnsZone zone = _entityMgr.findById(DnsZone.class, 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;
+ }
+
+ @Override
+ public String getEventDescription() {
+ return "Deleting DNS Zone ID: " + getId();
+ }
+
+ public Boolean isUnmanage() {
+ return Boolean.TRUE.equals(unmanage);
+ }
+}
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 00000000000..b0aa0902e9e
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java
@@ -0,0 +1,80 @@
+// 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.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;
+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 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.network.Network;
+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})
+public class DisassociateDnsZoneFromNetworkCmd extends BaseCmd {
+
+ @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;
+
+ @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() {
+ Network network = _entityMgr.findById(Network.class, networkId);
+ if (network != null) {
+ return network.getAccountId();
+ }
+ return Account.ACCOUNT_ID_SYSTEM;
+ }
+
+ public Long getNetworkId() {
+ return networkId;
+ }
+}
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 00000000000..800f030754b
--- /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 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.DnsProvider;
+
+@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 {
+
+ @Override
+ public void execute() {
+ List<String> providers = dnsProviderManager.listProviderNames();
+ ListResponse<DnsProviderResponse> response = new ListResponse<>();
+ List<DnsProviderResponse> responses = new ArrayList<>();
+ for (String name : providers) {
+ DnsProviderResponse resp = new DnsProviderResponse(name);
+ resp.setName(name);
+ 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 00000000000..246c8bee9aa
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java
@@ -0,0 +1,54 @@
+// 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.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;
+import org.apache.cloudstack.dns.DnsRecord;
+
+@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 {
+
+ @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
+ public void execute() {
+ // The manager will fetch live data from the plugin
+ ListResponse<DnsRecordResponse> response = dnsProviderManager.listDnsRecords(this);
+ response.setResponseName(getCommandName());
+ setResponseObject(response);
+ }
+}
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 00000000000..ca7ac1944a9
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java
@@ -0,0 +1,82 @@
+// 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.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.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,
+ 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 {
+
+ /////////////////////////////////////////////////////
+ //////////////// 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, type = CommandType.STRING,
+ description = "filter by provider type (e.g. PowerDNS, Cloudflare)")
+ private String providerType;
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ public Long getId() {
+ return id;
+ }
+
+ 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;
+ }
+
+ /////////////////////////////////////////////////////
+ /////////////// Implementation //////////////////////
+ /////////////////////////////////////////////////////
+
+ @Override
+ public void execute() {
+ ListResponse<DnsServerResponse> 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/ListDnsZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java
new file mode 100644
index 00000000000..e71bdabaf61
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java
@@ -0,0 +1,63 @@
+// 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.BaseListCmd;
+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.dns.DnsZone;
+
+@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 BaseListCmd {
+
+ /////////////////////////////////////////////////////
+ //////////////// 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.NAME, type = CommandType.STRING, description = "List by zone name")
+ private String name;
+
+ public Long getId() { return id; }
+ public Long getDnsServerId() { return dnsServerId; }
+ public String getName() { return name; }
+
+ @Override
+ public void execute() {
+ ListResponse<DnsZoneResponse> response = dnsProviderManager.listDnsZones(this);
+ response.setResponseName(getCommandName());
+ 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 00000000000..6b790fa8ade
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java
@@ -0,0 +1,150 @@
+// 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;
+
+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;
+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.dns.DnsServer;
+import org.apache.commons.lang3.StringUtils;
+
+import com.cloud.user.Account;
+import com.cloud.utils.EnumUtils;
+
+@APICommand(name = "updateDnsServer",
+ description = "Update DNS server",
+ responseObject = DnsServerResponse.class,
+ entityType = {DnsServer.class},
+ requestHasSensitiveInfo = true, responseHasSensitiveInfo = false,
+ since = "4.23.0",
+ authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class UpdateDnsServerCmd extends BaseCmd {
+
+ /////////////////////////////////////////////////////
+ //////////////// 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;
+
+ @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.DNS_API_KEY, type = CommandType.STRING, description = "API Key or Credentials for the external provider")
+ private String dnsApiKey;
+
+ @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 this DNS server can be used by accounts other than the owner to create and manage DNS zones")
+ private Boolean isPublic;
+
+ @Parameter(name = ApiConstants.PUBLIC_DOMAIN_SUFFIX, type = CommandType.STRING,
+ description = "Domain suffix that restricts DNS zones created by non-owner accounts to subdomains of this " +
+ "suffix (for example, sub.example.com under example.com)")
+ private String publicDomainSuffix;
+
+ @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.LIST, collectionType = CommandType.STRING,
+ description = "Comma separated list of name servers; used to create NS records for the DNS Zone (for example, ns1.example.com, ns2.example.com)")
+ private List<String> nameServers;
+
+ @Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "Update state for the DNS server (Enabled, Disabled)")
+ private String state;
+
+ /////////////////////////////////////////////////////
+ /////////////////// Accessors ///////////////////////
+ /////////////////////////////////////////////////////
+
+ public Long getId() { return id; }
+ public String getName() { return name; }
+ public String getUrl() { return url; }
+ public String getDnsApiKey() {
+ return dnsApiKey;
+ }
+ public Integer getPort() {
+ return port;
+ }
+ public Boolean isPublic() {
+ return isPublic;
+ }
+ public String getPublicDomainSuffix() {
+ return publicDomainSuffix;
+ }
+ public String getNameServers() {
+ if (nameServers == null) {
+ return null;
+ }
+ return StringUtils.join(nameServers.stream()
+ .filter(StringUtils::isNotBlank)
+ .map(StringUtils::trim)
+ .toArray(String[]::new), ",");
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ 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
+ 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 update DNS server", ex);
+ 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/command/user/dns/UpdateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java
new file mode 100644
index 00000000000..5952b73ea6e
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java
@@ -0,0 +1,95 @@
+// 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.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;
+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.dns.DnsZone;
+
+import com.cloud.user.Account;
+
+@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 {
+
+ /////////////////////////////////////////////////////
+ //////////////// API Parameters /////////////////////
+ /////////////////////////////////////////////////////
+
+ @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;
+
+ @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the DNS zone to be updated")
+ 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 long getEntityOwnerId() {
+ DnsZone dnsZone = _entityMgr.findById(DnsZone.class, id);
+ if (dnsZone != null) {
+ return dnsZone.getAccountId();
+ }
+ return Account.ACCOUNT_ID_SYSTEM;
+ }
+}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java
similarity index 53%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java
copy to api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java
index 2a0b02aae1d..f3a571f4063 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java
@@ -14,20 +14,32 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
+package org.apache.cloudstack.api.response;
-import org.springframework.stereotype.Component;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase;
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
-import com.cloud.vm.NicDetailVO;
+public class DnsProviderResponse extends BaseResponse {
-@Component
-public class NicDetailsDaoImpl extends ResourceDetailsDaoBase<NicDetailVO> implements NicDetailsDao {
+ @SerializedName(ApiConstants.NAME)
+ @Param(description = "The name of the DNS provider (e.g. PowerDNS, Cloudflare)")
+ private String name;
- @Override
- public void addDetail(long resourceId, String key, String value, boolean display) {
- super.addDetail(new NicDetailVO(resourceId, key, value, display));
+ 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 00000000000..6e208248551
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java
@@ -0,0 +1,57 @@
+// 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 java.util.List;
+
+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 record name (e.g., www.example.com.)")
+ private String name;
+
+ @SerializedName(ApiConstants.TYPE)
+ @Param(description = "The record type (e.g., A, CNAME, TXT)")
+ private DnsRecord.RecordType type;
+
+ @SerializedName("contents")
+ @Param(description = "The contents of the record (IP address or target)")
+ private List<String> contents;
+
+ @SerializedName("ttl")
+ @Param(description = "Time to live (TTL) in seconds")
+ private Integer ttl;
+
+ public DnsRecordResponse() {
+ super();
+ setObjectName("dnsrecord");
+ }
+
+ // Setters
+ public void setName(String name) { this.name = name; }
+ public void setType(DnsRecord.RecordType type) { this.type = type; }
+ public void setContent(List<String> contents) { this.contents = contents; }
+ public void setTtl(Integer ttl) { this.ttl = ttl; }
+}
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 00000000000..fecf49f0f98
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java
@@ -0,0 +1,133 @@
+// 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 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.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 = "ID of the DNS server")
+ private String id;
+
+ @SerializedName(ApiConstants.NAME)
+ @Param(description = "Name of the DNS server")
+ private String name;
+
+ @SerializedName(ApiConstants.URL)
+ @Param(description = "URL of the DNS server API")
+ private String url;
+
+ @SerializedName(ApiConstants.PORT)
+ @Param(description = "The port of the DNS server")
+ private Integer port;
+
+ @SerializedName(ApiConstants.PROVIDER)
+ @Param(description = "The provider type of the DNS server")
+ private String provider;
+
+ @SerializedName(ApiConstants.IS_PUBLIC)
+ @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")
+ private String publicDomainSuffix;
+
+ @SerializedName(ApiConstants.NAME_SERVERS)
+ @Param(description = "Name servers entries associated to DNS server")
+ private List<String> 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;
+
+ @SerializedName(ApiConstants.STATE)
+ @Param(description = "The state of the account")
+ private String state;
+
+ public DnsServerResponse() {
+ super();
+
+ }
+
+ 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(String 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<String> 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;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+}
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 00000000000..84fe46ce5ac
--- /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/api/response/DnsZoneResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java
new file mode 100644
index 00000000000..e179d2fd4b1
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java
@@ -0,0 +1,139 @@
+// 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 = "ID of the DNS zone")
+ private String id;
+
+ @SerializedName(ApiConstants.NAME)
+ @Param(description = "Name of the DNS zone")
+ private String name;
+
+ @SerializedName("dnsserverid")
+ @Param(description = "ID of the DNS server this zone belongs to")
+ 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")
+ private String networkId;
+
+ @SerializedName(ApiConstants.NETWORK_NAME)
+ @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 DnsZone.ZoneType type;
+
+ @SerializedName(ApiConstants.STATE)
+ @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");
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setDnsServerId(String dnsServerId) {
+ this.dnsServerId = dnsServerId;
+ }
+
+ 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;
+ }
+
+ public void setId(String id) {
+ this.id = 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/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java
index 3a3663af255..aeaf333540c 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java
@@ -335,6 +335,14 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement
@Param(description = ApiConstants.PARAMETER_DESCRIPTION_KEEP_MAC_ADDRESS_ON_PUBLIC_NIC, since = "4.23.0")
private Boolean keepMacAddressOnPublicNic;
+ @SerializedName(ApiConstants.DNS_ZONE)
+ @Param(description = "DNS zone associated to the network", since = "4.23.0")
+ private String dnsZone;
+
+ @SerializedName(ApiConstants.DNS_SUB_DOMAIN)
+ @Param(description = "DNS subdomain associated to the network", since = "4.23.0")
+ private String dnsSubdomain;
+
public NetworkResponse() {}
public Boolean getDisplayNetwork() {
@@ -710,4 +718,12 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement
public void setKeepMacAddressOnPublicNic(Boolean keepMacAddressOnPublicNic) {
this.keepMacAddressOnPublicNic = keepMacAddressOnPublicNic;
}
+
+ public void setDnsZone(String dnsZone) {
+ this.dnsZone = dnsZone;
+ }
+
+ public void setDnsSubdomain(String dnsSubdomain) {
+ this.dnsSubdomain = dnsSubdomain;
+ }
}
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 92f25e370fb..5ad41ad6224 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
@@ -150,6 +150,10 @@ public class NicResponse extends BaseResponse {
@Param(description = "whether the NIC is enabled or not")
private Boolean isEnabled;
+ @SerializedName(ApiConstants.NIC_DNS_NAME)
+ @Param(description = "DNS name associated with this NIC's IP address")
+ private String nicDnsName;
+
public void setVmId(String vmId) {
this.vmId = vmId;
}
@@ -428,4 +432,8 @@ public class NicResponse extends BaseResponse {
public void setEnabled(Boolean enabled) {
isEnabled = enabled;
}
+
+ public void setNicDnsName(String nicDnsName) {
+ this.nicDnsName = nicDnsName;
+ }
}
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 00000000000..231583244c2
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java
@@ -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.
+
+package org.apache.cloudstack.dns;
+
+import java.util.List;
+
+import org.apache.cloudstack.dns.exception.DnsProviderException;
+
+import com.cloud.utils.component.Adapter;
+
+public interface DnsProvider extends Adapter {
+ DnsProviderType getProviderType();
+
+ // 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;
+ void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException;
+
+ String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException;
+ List<DnsRecord> listRecords(DnsServer server, DnsZone zone) throws DnsProviderException;
+ String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException;
+ String deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException;
+ boolean dnsRecordExists(DnsServer server, DnsZone zone, String recordName, String recordType) throws DnsProviderException;
+ boolean dnsZoneExists(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 00000000000..5430884a32d
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java
@@ -0,0 +1,78 @@
+// 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.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;
+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.DnsZoneNetworkMapResponse;
+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;
+
+public interface DnsProviderManager extends Manager, PluggableService {
+
+ DnsServer addDnsServer(AddDnsServerCmd cmd);
+ ListResponse<DnsServerResponse> listDnsServers(ListDnsServersCmd cmd);
+ DnsServer updateDnsServer(UpdateDnsServerCmd cmd);
+ boolean deleteDnsServer(DeleteDnsServerCmd cmd);
+ DnsServerResponse createDnsServerResponse(DnsServer server);
+
+ // Allocates the DB row (State: Inactive)
+ DnsZone allocateDnsZone(CreateDnsZoneCmd cmd);
+ // Calls the Plugin (State: Inactive -> Active)
+ DnsZone provisionDnsZone(long zoneId, boolean isImport);
+
+ DnsZone updateDnsZone(UpdateDnsZoneCmd cmd);
+ boolean deleteDnsZone(Long id, boolean isUnmanage);
+ ListResponse<DnsZoneResponse> listDnsZones(ListDnsZonesCmd cmd);
+
+ DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd);
+ boolean deleteDnsRecord(DeleteDnsRecordCmd cmd);
+ ListResponse<DnsRecordResponse> listDnsRecords(ListDnsRecordsCmd cmd);
+
+ List<String> listProviderNames();
+
+ // Helper to create the response object
+ DnsZoneResponse createDnsZoneResponse(DnsZone zone);
+ DnsRecordResponse createDnsRecordResponse(DnsRecord record);
+
+ DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetworkCmd cmd);
+
+ boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd);
+
+ void checkDnsServerPermission(Account caller, DnsServer dnsServer);
+
+ void checkDnsZonePermission(Account caller, DnsZone dnsZone);
+}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java
similarity index 75%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java
index 2ca901fa3e5..23c8e936613 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java
@@ -14,12 +14,10 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns;
-import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
-
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+public enum DnsProviderType {
+ PowerDNS;
+// Cloudflare
}
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 00000000000..ae62e4729cc
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java
@@ -0,0 +1,66 @@
+// 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 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;
+ private List<String> contents;
+ private int ttl;
+
+ public DnsRecord() {}
+
+ public DnsRecord(String name, RecordType type, List<String> contents, int ttl) {
+ this.name = name;
+ this.type = type;
+ this.contents = contents;
+ 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 List<String> getContents() { return contents; }
+ public void setContents(List<String> 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
new file mode 100644
index 00000000000..f34f94726d1
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.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.dns;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cloudstack.acl.ControlledEntity;
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+public interface DnsServer extends InternalIdentity, Identity, ControlledEntity {
+ enum State {
+ Enabled, Disabled
+ };
+
+ String getName();
+
+ String getUrl();
+
+ DnsProviderType getProviderType();
+
+ List<String> getNameServers();
+
+ String getDnsApiKey();
+
+ long getAccountId();
+
+ boolean getPublicServer();
+
+ Date getCreated();
+
+ Date getRemoved();
+
+ String getPublicDomainSuffix();
+
+ Integer getPort();
+
+ Map<String, String> getDetails();
+
+ String getDetail(String name);
+
+ void setDetails(Map<String, String> details);
+
+ void appendDetails(String name, String value);
+}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java
similarity index 58%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java
copy to api/src/main/java/org/apache/cloudstack/dns/DnsZone.java
index 2a0b02aae1d..193185a6b0c 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java
+++ b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java
@@ -14,20 +14,34 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
+package org.apache.cloudstack.dns;
-import org.springframework.stereotype.Component;
+import java.util.List;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase;
+import org.apache.cloudstack.acl.ControlledEntity;
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
-import com.cloud.vm.NicDetailVO;
+public interface DnsZone extends InternalIdentity, Identity, ControlledEntity {
+ enum ZoneType {
+ Public, Private
+ }
+ enum State {
+ Active, Inactive
+ }
-@Component
-public class NicDetailsDaoImpl extends ResourceDetailsDaoBase<NicDetailVO> implements NicDetailsDao {
+ String getName();
- @Override
- public void addDetail(long resourceId, String key, String value, boolean display) {
- super.addDetail(new NicDetailVO(resourceId, key, value, display));
- }
+ long getDnsServerId();
+
+ long getAccountId();
+
+ ZoneType getType();
+
+ String getDescription();
+
+ List<Long> getAssociatedNetworks();
+
+ State getState();
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java
similarity index 75%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java
index 2ca901fa3e5..325cb78241e 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java
@@ -14,12 +14,14 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.exception;
-import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
-
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+/**
+ * Thrown when authentication to the DNS provider fails.
+ */
+public class DnsAuthenticationException extends DnsProviderException {
+ public DnsAuthenticationException(String message) {
+ super(message);
+ }
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java
similarity index 74%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java
index 2ca901fa3e5..9a36bb87478 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java
@@ -14,12 +14,14 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.exception;
-import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
-
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+/**
+ * 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/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java
similarity index 75%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java
index 2ca901fa3e5..aa88f308ce8 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java
@@ -14,12 +14,14 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.exception;
-import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
-
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+/**
+ * Thrown when the requested zone or record does not exist.
+ */
+public class DnsNotFoundException extends DnsProviderException {
+ public DnsNotFoundException(String message) {
+ super(message);
+ }
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java
similarity index 74%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java
index 2ca901fa3e5..564acdc9a6f 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java
@@ -14,12 +14,14 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.exception;
-import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
-
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+/**
+ * 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/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java
similarity index 74%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java
index 2ca901fa3e5..de307c9903e 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java
@@ -14,12 +14,15 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.exception;
-import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
+public class DnsProviderException extends Exception {
+ public DnsProviderException(String message) {
+ super(message);
+ }
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+ public DnsProviderException(String message, Throwable cause) {
+ super(message, cause);
+ }
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java
similarity index 70%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java
index 2ca901fa3e5..50f04143c92 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java
@@ -14,12 +14,17 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.exception;
-import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
+import java.io.IOException;
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+/**
+ * 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/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmdTest.java
new file mode 100644
index 00000000000..2824a8dacb9
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmdTest.java
@@ -0,0 +1,134 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.DnsServerResponse;
+import org.apache.cloudstack.dns.DnsProviderType;
+import org.apache.cloudstack.dns.DnsServer;
+import org.junit.Test;
+
+public class AddDnsServerCmdTest extends BaseDnsCmdTest {
+
+ private AddDnsServerCmd createCmd() throws Exception {
+ AddDnsServerCmd cmd = new AddDnsServerCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "name", "test-dns");
+ setField(cmd, "url", "http://dns.example.com");
+ setField(cmd, "provider", "PowerDNS");
+ setField(cmd, "dnsApiKey", "api-key-123");
+ setField(cmd, "port", 8081);
+ setField(cmd, "isPublic", true);
+ setField(cmd, "publicDomainSuffix", "public.example.com");
+ setField(cmd, "nameServers", Arrays.asList("ns1.example.com", "ns2.example.com"));
+ setField(cmd, "dnsUserName", "admin@example.com");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ AddDnsServerCmd cmd = createCmd();
+
+ assertEquals("test-dns", cmd.getName());
+ assertEquals("http://dns.example.com", cmd.getUrl());
+ assertEquals("api-key-123", cmd.getDnsApiKey());
+ assertEquals(Integer.valueOf(8081), cmd.getPort());
+ assertTrue(cmd.isPublic());
+ assertEquals("public.example.com", cmd.getPublicDomainSuffix());
+ assertEquals(Arrays.asList("ns1.example.com", "ns2.example.com"), cmd.getNameServers());
+ assertEquals(DnsProviderType.PowerDNS, cmd.getProvider());
+ assertEquals("admin@example.com", cmd.getDnsUserName());
+ }
+
+ @Test
+ public void testIsPublicFalse() throws Exception {
+ AddDnsServerCmd cmd = createCmd();
+ setField(cmd, "isPublic", false);
+ assertFalse(cmd.isPublic());
+ }
+
+ @Test
+ public void testIsPublicNull() throws Exception {
+ AddDnsServerCmd cmd = createCmd();
+ setField(cmd, "isPublic", null);
+ assertFalse(cmd.isPublic());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ AddDnsServerCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetProviderDefault() throws Exception {
+ AddDnsServerCmd cmd = createCmd();
+ setField(cmd, "provider", null);
+ assertEquals(DnsProviderType.PowerDNS, cmd.getProvider());
+ }
+
+ @Test
+ public void testGetProviderCaseInsensitive() throws Exception {
+ AddDnsServerCmd cmd = createCmd();
+ setField(cmd, "provider", "powerdns");
+ assertEquals(DnsProviderType.PowerDNS, cmd.getProvider());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ AddDnsServerCmd cmd = createCmd();
+
+ DnsServer mockServer = mock(DnsServer.class);
+ DnsServerResponse mockResponse = new DnsServerResponse();
+ mockResponse.setName("test-dns");
+
+ when(dnsProviderManager.addDnsServer(cmd)).thenReturn(mockServer);
+ when(dnsProviderManager.createDnsServerResponse(mockServer)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ DnsServerResponse response = (DnsServerResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("adddnsserverresponse", response.getResponseName());
+ verify(dnsProviderManager).addDnsServer(cmd);
+ verify(dnsProviderManager).createDnsServerResponse(mockServer);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ AddDnsServerCmd cmd = createCmd();
+ when(dnsProviderManager.addDnsServer(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteThrowsException() throws Exception {
+ AddDnsServerCmd cmd = createCmd();
+ when(dnsProviderManager.addDnsServer(cmd)).thenThrow(new RuntimeException("Connection refused"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmdTest.java
new file mode 100644
index 00000000000..593a0a041ce
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmdTest.java
@@ -0,0 +1,97 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse;
+import org.junit.Test;
+
+import com.cloud.network.Network;
+import com.cloud.user.Account;
+
+public class AssociateDnsZoneToNetworkCmdTest extends BaseDnsCmdTest {
+
+ private static final long NETWORK_ID = 200L;
+
+ private AssociateDnsZoneToNetworkCmd createCmd() throws Exception {
+ AssociateDnsZoneToNetworkCmd cmd = new AssociateDnsZoneToNetworkCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "_entityMgr", entityManager);
+ setField(cmd, "dnsZoneId", ENTITY_ID);
+ setField(cmd, "networkId", NETWORK_ID);
+ setField(cmd, "subDomain", "dev");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ AssociateDnsZoneToNetworkCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getDnsZoneId());
+ assertEquals(Long.valueOf(NETWORK_ID), cmd.getNetworkId());
+ assertEquals("dev", cmd.getSubDomain());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdWithNetwork() throws Exception {
+ AssociateDnsZoneToNetworkCmd cmd = createCmd();
+ Network mockNetwork = mock(Network.class);
+ when(mockNetwork.getAccountId()).thenReturn(ACCOUNT_ID);
+ when(entityManager.findById(Network.class, NETWORK_ID))
+ .thenReturn(mockNetwork);
+
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdNetworkNotFound() throws Exception {
+ AssociateDnsZoneToNetworkCmd cmd = createCmd();
+ when(entityManager.findById(Network.class, NETWORK_ID))
+ .thenReturn(null);
+
+ assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ AssociateDnsZoneToNetworkCmd cmd = createCmd();
+ DnsZoneNetworkMapResponse mockResponse =
+ new DnsZoneNetworkMapResponse();
+ when(dnsProviderManager.associateZoneToNetwork(cmd))
+ .thenReturn(mockResponse);
+
+ cmd.execute();
+
+ DnsZoneNetworkMapResponse response =
+ (DnsZoneNetworkMapResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("associatednszonetonetworkresponse",
+ response.getResponseName());
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteThrowsException() throws Exception {
+ AssociateDnsZoneToNetworkCmd cmd = createCmd();
+ when(dnsProviderManager.associateZoneToNetwork(cmd))
+ .thenThrow(new RuntimeException("Error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/BaseDnsCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/BaseDnsCmdTest.java
new file mode 100644
index 00000000000..e8b611ca17c
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/BaseDnsCmdTest.java
@@ -0,0 +1,88 @@
+// 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 static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.dns.DnsProviderManager;
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import com.cloud.user.Account;
+import com.cloud.utils.db.EntityManager;
+
+/**
+ * Shared setup for all DNS command unit tests.
+ */
+public abstract class BaseDnsCmdTest {
+
+ protected static final long ACCOUNT_ID = 42L;
+ protected static final long ENTITY_ID = 100L;
+
+ protected DnsProviderManager dnsProviderManager;
+ protected EntityManager entityManager;
+ protected Account callingAccount;
+
+ private MockedStatic<CallContext> callContextMock;
+
+ @Before
+ public void setUp() {
+ dnsProviderManager = mock(DnsProviderManager.class);
+ entityManager = mock(EntityManager.class);
+
+ callingAccount = mock(Account.class);
+ when(callingAccount.getId()).thenReturn(ACCOUNT_ID);
+
+ CallContext callContext = mock(CallContext.class);
+ when(callContext.getCallingAccount()).thenReturn(callingAccount);
+
+ callContextMock = Mockito.mockStatic(CallContext.class);
+ callContextMock.when(CallContext::current).thenReturn(callContext);
+ }
+
+ @After
+ public void tearDown() {
+ callContextMock.close();
+ }
+
+ /**
+ * Sets a private/inherited field value via reflection.
+ */
+ protected void setField(Object target, String fieldName, Object value) throws Exception {
+ Field field = null;
+ Class<?> clazz = target.getClass();
+ while (clazz != null) {
+ try {
+ field = clazz.getDeclaredField(fieldName);
+ break;
+ } catch (NoSuchFieldException e) {
+ clazz = clazz.getSuperclass();
+ }
+ }
+ if (field == null) {
+ throw new NoSuchFieldException(fieldName + " not found in hierarchy of " + target.getClass().getName());
+ }
+ field.setAccessible(true);
+ field.set(target, value);
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmdTest.java
new file mode 100644
index 00000000000..c37930dd228
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmdTest.java
@@ -0,0 +1,117 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.DnsRecordResponse;
+import org.apache.cloudstack.dns.DnsRecord;
+import org.junit.Test;
+
+import com.cloud.event.EventTypes;
+import com.cloud.exception.InvalidParameterValueException;
+
+public class CreateDnsRecordCmdTest extends BaseDnsCmdTest {
+
+ private CreateDnsRecordCmd createCmd() throws Exception {
+ CreateDnsRecordCmd cmd = new CreateDnsRecordCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "dnsZoneId", ENTITY_ID);
+ setField(cmd, "name", "www");
+ setField(cmd, "type", "A");
+ setField(cmd, "contents", Arrays.asList("192.168.1.1"));
+ setField(cmd, "ttl", 7200);
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ CreateDnsRecordCmd cmd = createCmd();
+
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getDnsZoneId());
+ assertEquals("www", cmd.getName());
+ assertEquals(DnsRecord.RecordType.A, cmd.getType());
+ assertEquals(Arrays.asList("192.168.1.1"), cmd.getContents());
+ assertEquals(Integer.valueOf(7200), cmd.getTtl());
+ }
+
+ @Test
+ public void testGetTtlDefault() throws Exception {
+ CreateDnsRecordCmd cmd = createCmd();
+ setField(cmd, "ttl", null);
+ assertEquals(Integer.valueOf(3600), cmd.getTtl());
+ }
+
+ @Test
+ public void testGetTypeCname() throws Exception {
+ CreateDnsRecordCmd cmd = createCmd();
+ setField(cmd, "type", "CNAME");
+ assertEquals(DnsRecord.RecordType.CNAME, cmd.getType());
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testGetTypeInvalid() throws Exception {
+ CreateDnsRecordCmd cmd = createCmd();
+ setField(cmd, "type", "INVALID");
+ cmd.getType();
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ CreateDnsRecordCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testEventType() throws Exception {
+ CreateDnsRecordCmd cmd = createCmd();
+ assertEquals(EventTypes.EVENT_DNS_RECORD_CREATE, cmd.getEventType());
+ }
+
+ @Test
+ public void testEventDescription() throws Exception {
+ CreateDnsRecordCmd cmd = createCmd();
+ assertEquals("Creating DNS Record: www", cmd.getEventDescription());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ CreateDnsRecordCmd cmd = createCmd();
+
+ DnsRecordResponse mockResponse = new DnsRecordResponse();
+ mockResponse.setName("www");
+ when(dnsProviderManager.createDnsRecord(cmd)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ DnsRecordResponse response = (DnsRecordResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("creatednsrecordresponse", response.getResponseName());
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteThrowsException() throws Exception {
+ CreateDnsRecordCmd cmd = createCmd();
+ when(dnsProviderManager.createDnsRecord(cmd)).thenThrow(new RuntimeException("Provider error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmdTest.java
new file mode 100644
index 00000000000..dc37874e659
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmdTest.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.api.command.user.dns;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.DnsZoneResponse;
+import org.apache.cloudstack.dns.DnsZone;
+import org.junit.Test;
+
+import com.cloud.event.EventTypes;
+
+public class CreateDnsZoneCmdTest extends BaseDnsCmdTest {
+
+ private CreateDnsZoneCmd createCmd() throws Exception {
+ CreateDnsZoneCmd cmd = new CreateDnsZoneCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "name", "example.com");
+ setField(cmd, "dnsServerId", ENTITY_ID);
+ setField(cmd, "type", "Public");
+ setField(cmd, "description", "Test zone");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+
+ assertEquals("example.com", cmd.getName());
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getDnsServerId());
+ assertEquals(DnsZone.ZoneType.Public, cmd.getType());
+ assertEquals("Test zone", cmd.getDescription());
+ }
+
+ @Test
+ public void testGetTypePrivate() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ setField(cmd, "type", "Private");
+ assertEquals(DnsZone.ZoneType.Private, cmd.getType());
+ }
+
+ @Test
+ public void testGetTypeDefaultsToPublicWhenNull() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ setField(cmd, "type", null);
+ assertEquals(DnsZone.ZoneType.Public, cmd.getType());
+ }
+
+ @Test
+ public void testGetTypeDefaultsToPublicWhenBlank() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ setField(cmd, "type", "");
+ assertEquals(DnsZone.ZoneType.Public, cmd.getType());
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testEventType() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ assertEquals(EventTypes.EVENT_DNS_ZONE_CREATE, cmd.getEventType());
+ }
+
+ @Test
+ public void testEventDescription() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ assertEquals("creating DNS zone: example.com", cmd.getEventDescription());
+ }
+
+ @Test
+ public void testCreateSuccess() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+
+ DnsZone mockZone = mock(DnsZone.class);
+ when(mockZone.getId()).thenReturn(ENTITY_ID);
+ when(mockZone.getUuid()).thenReturn("uuid-123");
+ when(dnsProviderManager.allocateDnsZone(cmd)).thenReturn(mockZone);
+
+ cmd.create();
+
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getEntityId());
+ assertEquals("uuid-123", cmd.getEntityUuid());
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testCreateReturnsNull() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ when(dnsProviderManager.allocateDnsZone(cmd)).thenReturn(null);
+ cmd.create();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testCreateThrowsException() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ when(dnsProviderManager.allocateDnsZone(cmd)).thenThrow(new RuntimeException("DB error"));
+ cmd.create();
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ cmd.setEntityId(ENTITY_ID);
+
+ DnsZone mockZone = mock(DnsZone.class);
+ DnsZoneResponse mockResponse = new DnsZoneResponse();
+ mockResponse.setName("example.com");
+
+ when(dnsProviderManager.provisionDnsZone(ENTITY_ID, false)).thenReturn(mockZone);
+ when(dnsProviderManager.createDnsZoneResponse(mockZone)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ DnsZoneResponse response = (DnsZoneResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("creatednszoneresponse", response.getResponseName());
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ cmd.setEntityId(ENTITY_ID);
+
+ when(dnsProviderManager.provisionDnsZone(ENTITY_ID, false)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteThrowsException() throws Exception {
+ CreateDnsZoneCmd cmd = createCmd();
+ cmd.setEntityId(ENTITY_ID);
+
+ when(dnsProviderManager.provisionDnsZone(ENTITY_ID, false)).thenThrow(new RuntimeException("Provider error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmdTest.java
new file mode 100644
index 00000000000..46aacf5a68c
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmdTest.java
@@ -0,0 +1,102 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.dns.DnsRecord;
+import org.junit.Test;
+
+import com.cloud.event.EventTypes;
+import com.cloud.exception.InvalidParameterValueException;
+
+public class DeleteDnsRecordCmdTest extends BaseDnsCmdTest {
+
+ private DeleteDnsRecordCmd createCmd() throws Exception {
+ DeleteDnsRecordCmd cmd = new DeleteDnsRecordCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "dnsZoneId", ENTITY_ID);
+ setField(cmd, "name", "www");
+ setField(cmd, "type", "A");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ DeleteDnsRecordCmd cmd = createCmd();
+
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getDnsZoneId());
+ assertEquals("www", cmd.getName());
+ assertEquals(DnsRecord.RecordType.A, cmd.getType());
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testGetTypeInvalid() throws Exception {
+ DeleteDnsRecordCmd cmd = createCmd();
+ setField(cmd, "type", "BOGUS");
+ cmd.getType();
+ }
+
+ @Test
+ public void testGetEntityOwnerId() throws Exception {
+ DeleteDnsRecordCmd cmd = createCmd();
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testEventType() throws Exception {
+ DeleteDnsRecordCmd cmd = createCmd();
+ assertEquals(EventTypes.EVENT_DNS_RECORD_DELETE, cmd.getEventType());
+ }
+
+ @Test
+ public void testEventDescription() throws Exception {
+ DeleteDnsRecordCmd cmd = createCmd();
+ assertEquals("Deleting DNS Record: www", cmd.getEventDescription());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ DeleteDnsRecordCmd cmd = createCmd();
+ when(dnsProviderManager.deleteDnsRecord(cmd)).thenReturn(true);
+
+ cmd.execute();
+
+ SuccessResponse response = (SuccessResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ verify(dnsProviderManager).deleteDnsRecord(cmd);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsFalse() throws Exception {
+ DeleteDnsRecordCmd cmd = createCmd();
+ when(dnsProviderManager.deleteDnsRecord(cmd)).thenReturn(false);
+ cmd.execute();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteThrowsException() throws Exception {
+ DeleteDnsRecordCmd cmd = createCmd();
+ when(dnsProviderManager.deleteDnsRecord(cmd)).thenThrow(new RuntimeException("Error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmdTest.java
new file mode 100644
index 00000000000..adebd339d71
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmdTest.java
@@ -0,0 +1,119 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.dns.DnsServer;
+import org.junit.Test;
+
+import com.cloud.event.EventTypes;
+import com.cloud.user.Account;
+
+public class DeleteDnsServerCmdTest extends BaseDnsCmdTest {
+
+ private DeleteDnsServerCmd createCmd() throws Exception {
+ DeleteDnsServerCmd cmd = new DeleteDnsServerCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "_entityMgr", entityManager);
+ setField(cmd, "id", ENTITY_ID);
+ return cmd;
+ }
+
+ @Test
+ public void testGetId() throws Exception {
+ DeleteDnsServerCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ }
+
+ @Test
+ public void testGetCleanupDefault() throws Exception {
+ DeleteDnsServerCmd cmd = createCmd();
+ assertTrue(cmd.getCleanup());
+ }
+
+ @Test
+ public void testGetCleanupFalse() throws Exception {
+ DeleteDnsServerCmd cmd = createCmd();
+ setField(cmd, "cleanup", false);
+ assertFalse(cmd.getCleanup());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdWithServer() throws Exception {
+ DeleteDnsServerCmd cmd = createCmd();
+ DnsServer mockServer = mock(DnsServer.class);
+ when(mockServer.getAccountId()).thenReturn(ACCOUNT_ID);
+ when(entityManager.findById(DnsServer.class, ENTITY_ID)).thenReturn(mockServer);
+
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdServerNotFound() throws Exception {
+ DeleteDnsServerCmd cmd = createCmd();
+ when(entityManager.findById(DnsServer.class, ENTITY_ID)).thenReturn(null);
+
+ assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testEventType() throws Exception {
+ DeleteDnsServerCmd cmd = createCmd();
+ assertEquals(EventTypes.EVENT_DNS_SERVER_DELETE, cmd.getEventType());
+ }
+
+ @Test
+ public void testEventDescription() throws Exception {
+ DeleteDnsServerCmd cmd = createCmd();
+ assertEquals("Deleting DNS server ID: " + ENTITY_ID, cmd.getEventDescription());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ DeleteDnsServerCmd cmd = createCmd();
+ when(dnsProviderManager.deleteDnsServer(cmd)).thenReturn(true);
+
+ cmd.execute();
+
+ SuccessResponse response = (SuccessResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ verify(dnsProviderManager).deleteDnsServer(cmd);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsFalse() throws Exception {
+ DeleteDnsServerCmd cmd = createCmd();
+ when(dnsProviderManager.deleteDnsServer(cmd)).thenReturn(false);
+ cmd.execute();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteThrowsException() throws Exception {
+ DeleteDnsServerCmd cmd = createCmd();
+ when(dnsProviderManager.deleteDnsServer(cmd)).thenThrow(new RuntimeException("Error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmdTest.java
new file mode 100644
index 00000000000..350cea76e5c
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmdTest.java
@@ -0,0 +1,104 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.dns.DnsZone;
+import org.junit.Test;
+
+import com.cloud.event.EventTypes;
+import com.cloud.user.Account;
+
+public class DeleteDnsZoneCmdTest extends BaseDnsCmdTest {
+
+ private DeleteDnsZoneCmd createCmd() throws Exception {
+ DeleteDnsZoneCmd cmd = new DeleteDnsZoneCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "_entityMgr", entityManager);
+ setField(cmd, "id", ENTITY_ID);
+ return cmd;
+ }
+
+ @Test
+ public void testGetId() throws Exception {
+ DeleteDnsZoneCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdWithZone() throws Exception {
+ DeleteDnsZoneCmd cmd = createCmd();
+ DnsZone mockZone = mock(DnsZone.class);
+ when(mockZone.getAccountId()).thenReturn(ACCOUNT_ID);
+ when(entityManager.findById(DnsZone.class, ENTITY_ID)).thenReturn(mockZone);
+
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdZoneNotFound() throws Exception {
+ DeleteDnsZoneCmd cmd = createCmd();
+ when(entityManager.findById(DnsZone.class, ENTITY_ID)).thenReturn(null);
+
+ assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testEventType() throws Exception {
+ DeleteDnsZoneCmd cmd = createCmd();
+ assertEquals(EventTypes.EVENT_DNS_ZONE_DELETE, cmd.getEventType());
+ }
+
+ @Test
+ public void testEventDescription() throws Exception {
+ DeleteDnsZoneCmd cmd = createCmd();
+ assertEquals("Deleting DNS Zone ID: " + ENTITY_ID, cmd.getEventDescription());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ DeleteDnsZoneCmd cmd = createCmd();
+ when(dnsProviderManager.deleteDnsZone(ENTITY_ID, false)).thenReturn(true);
+
+ cmd.execute();
+
+ SuccessResponse response = (SuccessResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ verify(dnsProviderManager).deleteDnsZone(ENTITY_ID, false);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsFalse() throws Exception {
+ DeleteDnsZoneCmd cmd = createCmd();
+ when(dnsProviderManager.deleteDnsZone(ENTITY_ID, false)).thenReturn(false);
+ cmd.execute();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteThrowsException() throws Exception {
+ DeleteDnsZoneCmd cmd = createCmd();
+ when(dnsProviderManager.deleteDnsZone(ENTITY_ID, false)).thenThrow(new RuntimeException("Error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmdTest.java
new file mode 100644
index 00000000000..34ef07efce3
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmdTest.java
@@ -0,0 +1,100 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.junit.Test;
+
+import com.cloud.network.Network;
+import com.cloud.user.Account;
+
+public class DisassociateDnsZoneFromNetworkCmdTest extends BaseDnsCmdTest {
+
+ private static final long NETWORK_ID = 200L;
+
+ private DisassociateDnsZoneFromNetworkCmd createCmd() throws Exception {
+ DisassociateDnsZoneFromNetworkCmd cmd =
+ new DisassociateDnsZoneFromNetworkCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "_entityMgr", entityManager);
+ setField(cmd, "networkId", NETWORK_ID);
+ return cmd;
+ }
+
+ @Test
+ public void testGetNetworkId() throws Exception {
+ DisassociateDnsZoneFromNetworkCmd cmd = createCmd();
+ assertEquals(Long.valueOf(NETWORK_ID), cmd.getNetworkId());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdWithNetwork() throws Exception {
+ DisassociateDnsZoneFromNetworkCmd cmd = createCmd();
+ Network mockNetwork = mock(Network.class);
+ when(mockNetwork.getAccountId()).thenReturn(ACCOUNT_ID);
+ when(entityManager.findById(Network.class, NETWORK_ID))
+ .thenReturn(mockNetwork);
+
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdNetworkNotFound() throws Exception {
+ DisassociateDnsZoneFromNetworkCmd cmd = createCmd();
+ when(entityManager.findById(Network.class, NETWORK_ID))
+ .thenReturn(null);
+
+ assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ DisassociateDnsZoneFromNetworkCmd cmd = createCmd();
+ when(dnsProviderManager.disassociateZoneFromNetwork(cmd))
+ .thenReturn(true);
+
+ cmd.execute();
+
+ SuccessResponse response =
+ (SuccessResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ verify(dnsProviderManager).disassociateZoneFromNetwork(cmd);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsFalse() throws Exception {
+ DisassociateDnsZoneFromNetworkCmd cmd = createCmd();
+ when(dnsProviderManager.disassociateZoneFromNetwork(cmd))
+ .thenReturn(false);
+ cmd.execute();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteThrowsException() throws Exception {
+ DisassociateDnsZoneFromNetworkCmd cmd = createCmd();
+ when(dnsProviderManager.disassociateZoneFromNetwork(cmd))
+ .thenThrow(new RuntimeException("Error"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmdTest.java
new file mode 100644
index 00000000000..62340742c87
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmdTest.java
@@ -0,0 +1,83 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.apache.cloudstack.api.response.DnsProviderResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.junit.Test;
+
+public class ListDnsProvidersCmdTest extends BaseDnsCmdTest {
+
+ private ListDnsProvidersCmd createCmd() throws Exception {
+ ListDnsProvidersCmd cmd = new ListDnsProvidersCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ return cmd;
+ }
+
+ @Test
+ public void testExecute() throws Exception {
+ ListDnsProvidersCmd cmd = createCmd();
+ when(dnsProviderManager.listProviderNames()).thenReturn(Arrays.asList("PowerDNS"));
+
+ cmd.execute();
+
+ @SuppressWarnings("unchecked")
+ ListResponse<DnsProviderResponse> response =
+ (ListResponse<DnsProviderResponse>) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("listdnsprovidersresponse", response.getResponseName());
+ assertNotNull(response.getResponses());
+ assertEquals(1, response.getResponses().size());
+ }
+
+ @Test
+ public void testExecuteMultipleProviders() throws Exception {
+ ListDnsProvidersCmd cmd = createCmd();
+ when(dnsProviderManager.listProviderNames())
+ .thenReturn(Arrays.asList("PowerDNS", "Cloudflare"));
+
+ cmd.execute();
+
+ @SuppressWarnings("unchecked")
+ ListResponse<DnsProviderResponse> response =
+ (ListResponse<DnsProviderResponse>) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals(2, response.getResponses().size());
+ }
+
+ @Test
+ public void testExecuteEmptyList() throws Exception {
+ ListDnsProvidersCmd cmd = createCmd();
+ when(dnsProviderManager.listProviderNames())
+ .thenReturn(Collections.emptyList());
+
+ cmd.execute();
+
+ @SuppressWarnings("unchecked")
+ ListResponse<DnsProviderResponse> response =
+ (ListResponse<DnsProviderResponse>) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals(0, response.getResponses().size());
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmdTest.java
new file mode 100644
index 00000000000..e75713b55d5
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmdTest.java
@@ -0,0 +1,56 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.response.DnsRecordResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.junit.Test;
+
+public class ListDnsRecordsCmdTest extends BaseDnsCmdTest {
+
+ private ListDnsRecordsCmd createCmd() throws Exception {
+ ListDnsRecordsCmd cmd = new ListDnsRecordsCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "dnsZoneId", ENTITY_ID);
+ return cmd;
+ }
+
+ @Test
+ public void testGetDnsZoneId() throws Exception {
+ ListDnsRecordsCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getDnsZoneId());
+ }
+
+ @Test
+ public void testExecute() throws Exception {
+ ListDnsRecordsCmd cmd = createCmd();
+
+ ListResponse<DnsRecordResponse> mockListResponse = new ListResponse<>();
+ when(dnsProviderManager.listDnsRecords(cmd)).thenReturn(mockListResponse);
+
+ cmd.execute();
+
+ @SuppressWarnings("unchecked")
+ ListResponse<DnsRecordResponse> response = (ListResponse<DnsRecordResponse>) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("listdnsrecordsresponse", response.getResponseName());
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmdTest.java
new file mode 100644
index 00000000000..710aa497167
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmdTest.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.command.user.dns;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.response.DnsServerResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.dns.DnsProviderType;
+import org.junit.Test;
+
+public class ListDnsServersCmdTest extends BaseDnsCmdTest {
+
+ private ListDnsServersCmd createCmd() throws Exception {
+ ListDnsServersCmd cmd = new ListDnsServersCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "id", ENTITY_ID);
+ setField(cmd, "providerType", "PowerDNS");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ ListDnsServersCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ assertEquals(DnsProviderType.PowerDNS, cmd.getProviderType());
+ }
+
+ @Test
+ public void testGetProviderTypeNull() throws Exception {
+ ListDnsServersCmd cmd = createCmd();
+ setField(cmd, "providerType", null);
+ assertEquals(DnsProviderType.PowerDNS, cmd.getProviderType());
+ }
+
+ @Test
+ public void testExecute() throws Exception {
+ ListDnsServersCmd cmd = createCmd();
+
+ ListResponse<DnsServerResponse> mockListResponse = new ListResponse<>();
+ when(dnsProviderManager.listDnsServers(cmd)).thenReturn(mockListResponse);
+
+ cmd.execute();
+
+ @SuppressWarnings("unchecked")
+ ListResponse<DnsServerResponse> response = (ListResponse<DnsServerResponse>) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("listdnsserversresponse", response.getResponseName());
+ assertEquals("dnsserver", response.getObjectName());
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmdTest.java
new file mode 100644
index 00000000000..7d47266ae6e
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmdTest.java
@@ -0,0 +1,60 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.response.DnsZoneResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.junit.Test;
+
+public class ListDnsZonesCmdTest extends BaseDnsCmdTest {
+
+ private ListDnsZonesCmd createCmd() throws Exception {
+ ListDnsZonesCmd cmd = new ListDnsZonesCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "id", ENTITY_ID);
+ setField(cmd, "dnsServerId", 200L);
+ setField(cmd, "name", "example.com");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ ListDnsZonesCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ assertEquals(Long.valueOf(200L), cmd.getDnsServerId());
+ assertEquals("example.com", cmd.getName());
+ }
+
+ @Test
+ public void testExecute() throws Exception {
+ ListDnsZonesCmd cmd = createCmd();
+
+ ListResponse<DnsZoneResponse> mockListResponse = new ListResponse<>();
+ when(dnsProviderManager.listDnsZones(cmd)).thenReturn(mockListResponse);
+
+ cmd.execute();
+
+ @SuppressWarnings("unchecked")
+ ListResponse<DnsZoneResponse> response = (ListResponse<DnsZoneResponse>) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("listdnszonesresponse", response.getResponseName());
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmdTest.java
new file mode 100644
index 00000000000..f3c0fcd4fe0
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmdTest.java
@@ -0,0 +1,145 @@
+// 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.DnsServerResponse;
+import org.apache.cloudstack.dns.DnsServer;
+import org.junit.Test;
+
+import com.cloud.user.Account;
+
+public class UpdateDnsServerCmdTest extends BaseDnsCmdTest {
+
+ private UpdateDnsServerCmd createCmd() throws Exception {
+ UpdateDnsServerCmd cmd = new UpdateDnsServerCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "_entityMgr", entityManager);
+ setField(cmd, "id", ENTITY_ID);
+ setField(cmd, "name", "updated-dns");
+ setField(cmd, "url", "http://updated.dns.com");
+ setField(cmd, "dnsApiKey", "new-api-key");
+ setField(cmd, "port", 9090);
+ setField(cmd, "isPublic", true);
+ setField(cmd, "publicDomainSuffix", "updated.example.com");
+ setField(cmd, "nameServers", Arrays.asList("ns1.updated.com", "ns2.updated.com"));
+ setField(cmd, "state", "Enabled");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ UpdateDnsServerCmd cmd = createCmd();
+
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ assertEquals("updated-dns", cmd.getName());
+ assertEquals("http://updated.dns.com", cmd.getUrl());
+ assertEquals("new-api-key", cmd.getDnsApiKey());
+ assertEquals(Integer.valueOf(9090), cmd.getPort());
+ assertTrue(cmd.isPublic());
+ assertEquals("updated.example.com", cmd.getPublicDomainSuffix());
+ assertEquals("ns1.updated.com,ns2.updated.com", cmd.getNameServers());
+ assertEquals(DnsServer.State.Enabled, cmd.getState());
+ }
+
+ @Test
+ public void testGetStateDisabled() throws Exception {
+ UpdateDnsServerCmd cmd = createCmd();
+ setField(cmd, "state", "Disabled");
+ assertEquals(DnsServer.State.Disabled, cmd.getState());
+ }
+
+ @Test
+ public void testGetStateNull() throws Exception {
+ UpdateDnsServerCmd cmd = createCmd();
+ setField(cmd, "state", null);
+ assertNull(cmd.getState());
+ }
+
+ @Test
+ public void testGetStateBlank() throws Exception {
+ UpdateDnsServerCmd cmd = createCmd();
+ setField(cmd, "state", "");
+ assertNull(cmd.getState());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetStateInvalid() throws Exception {
+ UpdateDnsServerCmd cmd = createCmd();
+ setField(cmd, "state", "InvalidState");
+ cmd.getState();
+ }
+
+ @Test
+ public void testGetEntityOwnerIdWithServer() throws Exception {
+ UpdateDnsServerCmd cmd = createCmd();
+ DnsServer mockServer = mock(DnsServer.class);
+ when(mockServer.getAccountId()).thenReturn(ACCOUNT_ID);
+ when(entityManager.findById(DnsServer.class, ENTITY_ID)).thenReturn(mockServer);
+
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdServerNotFound() throws Exception {
+ UpdateDnsServerCmd cmd = createCmd();
+ when(entityManager.findById(DnsServer.class, ENTITY_ID)).thenReturn(null);
+
+ assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ UpdateDnsServerCmd cmd = createCmd();
+
+ DnsServer mockServer = mock(DnsServer.class);
+ DnsServerResponse mockResponse = new DnsServerResponse();
+ mockResponse.setName("updated-dns");
+
+ when(dnsProviderManager.updateDnsServer(cmd)).thenReturn(mockServer);
+ when(dnsProviderManager.createDnsServerResponse(mockServer)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ DnsServerResponse response = (DnsServerResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("updatednsserverresponse", response.getResponseName());
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ UpdateDnsServerCmd cmd = createCmd();
+ when(dnsProviderManager.updateDnsServer(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteThrowsException() throws Exception {
+ UpdateDnsServerCmd cmd = createCmd();
+ when(dnsProviderManager.updateDnsServer(cmd)).thenThrow(new RuntimeException("Update failed"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmdTest.java
new file mode 100644
index 00000000000..cac16903185
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmdTest.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.api.command.user.dns;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.DnsZoneResponse;
+import org.apache.cloudstack.dns.DnsZone;
+import org.junit.Test;
+
+import com.cloud.user.Account;
+
+public class UpdateDnsZoneCmdTest extends BaseDnsCmdTest {
+
+ private UpdateDnsZoneCmd createCmd() throws Exception {
+ UpdateDnsZoneCmd cmd = new UpdateDnsZoneCmd();
+ setField(cmd, "dnsProviderManager", dnsProviderManager);
+ setField(cmd, "_entityMgr", entityManager);
+ setField(cmd, "id", ENTITY_ID);
+ setField(cmd, "description", "Updated description");
+ return cmd;
+ }
+
+ @Test
+ public void testAccessors() throws Exception {
+ UpdateDnsZoneCmd cmd = createCmd();
+ assertEquals(Long.valueOf(ENTITY_ID), cmd.getId());
+ assertEquals("Updated description", cmd.getDescription());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdWhenZoneExists() throws Exception {
+ UpdateDnsZoneCmd cmd = createCmd();
+ DnsZone mockZone = mock(DnsZone.class);
+ when(mockZone.getAccountId()).thenReturn(ACCOUNT_ID);
+ when(entityManager.findById(DnsZone.class, ENTITY_ID)).thenReturn(mockZone);
+ assertEquals(ACCOUNT_ID, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testGetEntityOwnerIdWhenZoneNotFound() throws Exception {
+ UpdateDnsZoneCmd cmd = createCmd();
+ when(entityManager.findById(DnsZone.class, ENTITY_ID)).thenReturn(null);
+ assertEquals(Account.ACCOUNT_ID_SYSTEM, cmd.getEntityOwnerId());
+ }
+
+ @Test
+ public void testExecuteSuccess() throws Exception {
+ UpdateDnsZoneCmd cmd = createCmd();
+
+ DnsZone mockZone = mock(DnsZone.class);
+ DnsZoneResponse mockResponse = new DnsZoneResponse();
+ mockResponse.setName("example.com");
+
+ when(dnsProviderManager.updateDnsZone(cmd)).thenReturn(mockZone);
+ when(dnsProviderManager.createDnsZoneResponse(mockZone)).thenReturn(mockResponse);
+
+ cmd.execute();
+
+ DnsZoneResponse response = (DnsZoneResponse) cmd.getResponseObject();
+ assertNotNull(response);
+ assertEquals("updatednszoneresponse", response.getResponseName());
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteReturnsNull() throws Exception {
+ UpdateDnsZoneCmd cmd = createCmd();
+ when(dnsProviderManager.updateDnsZone(cmd)).thenReturn(null);
+ cmd.execute();
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testExecuteThrowsException() throws Exception {
+ UpdateDnsZoneCmd cmd = createCmd();
+ when(dnsProviderManager.updateDnsZone(cmd)).thenThrow(new RuntimeException("Update failed"));
+ cmd.execute();
+ }
+}
diff --git a/api/src/test/java/org/apache/cloudstack/dns/DnsRecordTest.java b/api/src/test/java/org/apache/cloudstack/dns/DnsRecordTest.java
new file mode 100644
index 00000000000..bd993380e06
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/dns/DnsRecordTest.java
@@ -0,0 +1,105 @@
+// 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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Test;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+
+public class DnsRecordTest {
+
+ @Test
+ public void testDefaultConstructor() {
+ DnsRecord record = new DnsRecord();
+ assertNull(record.getName());
+ assertNull(record.getType());
+ assertNull(record.getContents());
+ assertEquals(0, record.getTtl());
+ }
+
+ @Test
+ public void testParameterizedConstructor() {
+ List<String> contents = Arrays.asList("192.168.1.1");
+ DnsRecord record = new DnsRecord("www", DnsRecord.RecordType.A, contents, 3600);
+
+ assertEquals("www", record.getName());
+ assertEquals(DnsRecord.RecordType.A, record.getType());
+ assertEquals(contents, record.getContents());
+ assertEquals(3600, record.getTtl());
+ }
+
+ @Test
+ public void testSettersAndGetters() {
+ DnsRecord record = new DnsRecord();
+ List<String> contents = Arrays.asList("10.0.0.1", "10.0.0.2");
+
+ record.setName("mail");
+ record.setType(DnsRecord.RecordType.AAAA);
+ record.setContents(contents);
+ record.setTtl(7200);
+
+ assertEquals("mail", record.getName());
+ assertEquals(DnsRecord.RecordType.AAAA, record.getType());
+ assertEquals(contents, record.getContents());
+ assertEquals(7200, record.getTtl());
+ }
+
+ // RecordType.fromString tests
+
+ @Test
+ public void testFromStringValid() {
+ assertEquals(DnsRecord.RecordType.A, DnsRecord.RecordType.fromString("A"));
+ assertEquals(DnsRecord.RecordType.AAAA, DnsRecord.RecordType.fromString("AAAA"));
+ assertEquals(DnsRecord.RecordType.CNAME, DnsRecord.RecordType.fromString("CNAME"));
+ assertEquals(DnsRecord.RecordType.MX, DnsRecord.RecordType.fromString("MX"));
+ assertEquals(DnsRecord.RecordType.TXT, DnsRecord.RecordType.fromString("TXT"));
+ assertEquals(DnsRecord.RecordType.SRV, DnsRecord.RecordType.fromString("SRV"));
+ assertEquals(DnsRecord.RecordType.PTR, DnsRecord.RecordType.fromString("PTR"));
+ assertEquals(DnsRecord.RecordType.NS, DnsRecord.RecordType.fromString("NS"));
+ }
+
+ @Test
+ public void testFromStringCaseInsensitive() {
+ assertEquals(DnsRecord.RecordType.A, DnsRecord.RecordType.fromString("a"));
+ assertEquals(DnsRecord.RecordType.CNAME, DnsRecord.RecordType.fromString("cname"));
+ assertEquals(DnsRecord.RecordType.MX, DnsRecord.RecordType.fromString("mx"));
+ }
+
+ @Test
+ public void testFromStringNull() {
+ assertNull(DnsRecord.RecordType.fromString(null));
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testFromStringInvalid() {
+ DnsRecord.RecordType.fromString("INVALID");
+ }
+
+ @Test
+ public void testRecordTypeValues() {
+ DnsRecord.RecordType[] values = DnsRecord.RecordType.values();
+ assertNotNull(values);
+ assertEquals(8, values.length);
+ }
+}
diff --git a/client/pom.xml b/client/pom.xml
index b7dbaa427c7..90b839112ce 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -677,6 +677,12 @@
<artifactId>cloud-utils</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-plugin-dns-powerdns</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
</dependencies>
<build>
<plugins>
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 15465c22f8f..0a92e8a637b 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
@@ -369,4 +369,6 @@
<bean id="kmsProvidersRegistry" class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
</bean>
+
+ <bean id="dnsProvidersRegistry" class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry" />
</beans>
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 00000000000..a2bb467be75
--- /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 00000000000..27cac940028
--- /dev/null
+++ b/core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml
@@ -0,0 +1,31 @@
+<!--
+
+ 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.
+
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
+>
+ <bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
+ <property name="registry" ref="dnsProvidersRegistry" />
+ <property name="typeClass" value="org.apache.cloudstack.dns.DnsProvider" />
+ </bean>
+
+</beans>
diff --git a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java
index 5eca02f33d5..c7238a51369 100644
--- a/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java
+++ b/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java
@@ -114,6 +114,8 @@ public interface VirtualMachineManager extends Manager {
interface Topics {
String VM_POWER_STATE = "vm.powerstate";
+ String VM_LIFECYCLE_STATE = "vm.lifecycle.state";
+ String VM_ACTION = "vm.action";
}
/**
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
index 2ca901fa3e5..403727ed9c6 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
@@ -16,10 +16,14 @@
// under the License.
package com.cloud.vm.dao;
+import java.util.Set;
+
import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
import com.cloud.utils.db.GenericDao;
import com.cloud.vm.NicDetailVO;
public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+
+ void removeDetailsForNicIds(String resourceName, Set<Long> nicIds);
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java
index 2a0b02aae1d..1011f0588bb 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java
@@ -17,17 +17,43 @@
package com.cloud.vm.dao;
-import org.springframework.stereotype.Component;
+import java.util.Set;
+import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase;
+import org.apache.commons.collections.CollectionUtils;
+import org.springframework.stereotype.Component;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
import com.cloud.vm.NicDetailVO;
@Component
public class NicDetailsDaoImpl extends ResourceDetailsDaoBase<NicDetailVO> implements NicDetailsDao {
+ private final SearchBuilder<NicDetailVO> ResourceIdNameSearch;
+
+ public NicDetailsDaoImpl() {
+ super();
+ ResourceIdNameSearch = createSearchBuilder();
+ ResourceIdNameSearch.and(ApiConstants.NAME, ResourceIdNameSearch.entity().getName(), SearchCriteria.Op.EQ);
+ ResourceIdNameSearch.and(ApiConstants.RESOURCE_ID, ResourceIdNameSearch.entity().getResourceId(), SearchCriteria.Op.IN);
+ ResourceIdNameSearch.done();
+ }
+
@Override
public void addDetail(long resourceId, String key, String value, boolean display) {
super.addDetail(new NicDetailVO(resourceId, key, value, display));
}
+
+ @Override
+ public void removeDetailsForNicIds(String resourceName, Set<Long> nicIds) {
+ if (CollectionUtils.isEmpty(nicIds)) {
+ return;
+ }
+ SearchCriteria<NicDetailVO> sc = ResourceIdNameSearch.create();
+ sc.setParameters(ApiConstants.NAME, resourceName);
+ sc.setParameters(ApiConstants.RESOURCE_ID, nicIds.toArray());
+ remove(sc);
+ }
}
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 ab166eff806..d7cacd09b9a 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
@@ -319,4 +319,12 @@
<bean id="kmsWrappedKeyDaoImpl" class="org.apache.cloudstack.kms.dao.KMSWrappedKeyDaoImpl" />
<bean id="hsmProfileDaoImpl" class="org.apache.cloudstack.kms.dao.HSMProfileDaoImpl" />
<bean id="hsmProfileDetailsDaoImpl" class="org.apache.cloudstack.kms.dao.HSMProfileDetailsDaoImpl" />
+ <bean id="dnsServerDao" class="org.apache.cloudstack.dns.dao.DnsServerDaoImpl" />
+ <bean id="dnsZoneDao" class="org.apache.cloudstack.dns.dao.DnsZoneDaoImpl" />
+ <bean id="dnsZoneNetworkMapDao" class="org.apache.cloudstack.dns.dao.DnsZoneNetworkMapDaoImpl" />
+ <bean id="dnsServerJoinDao" class="org.apache.cloudstack.dns.dao.DnsServerJoinDaoImpl" />
+ <bean id="dnsZoneJoinDao" class="org.apache.cloudstack.dns.dao.DnsZoneJoinDaoImpl" />
+ <bean id="dnsNicJoinDao" class="org.apache.cloudstack.dns.dao.NicDnsJoinDaoImpl" />
+ <bean id="dnsServerDetailsDao" class="org.apache.cloudstack.dns.dao.DnsServerDetailsDaoImpl" />
+
</beans>
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 3c434136961..c87697674a1 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
@@ -484,3 +484,81 @@ ADD UNIQUE KEY `load_balancer_id` (`load_balancer_id`, `instance_id`, `instance_
ALTER TABLE `cloud`.`global_load_balancer_lb_rule_map`
DROP KEY `gslb_rule_id`,
ADD UNIQUE KEY `gslb_rule_id` (`gslb_rule_id`, `lb_rule_id`, `removed`);
+
+-- ======================================================================
+-- DNS Framework Schema
+-- ======================================================================
+
+-- DNS Server Table (Stores DNS Server Configurations)
+CREATE TABLE IF NOT EXISTS `cloud`.`dns_server` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns server',
+ `uuid` varchar(40) COMMENT 'uuid of the dns server',
+ `name` varchar(255) NOT NULL COMMENT 'display name of the dns server',
+ `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) COMMENT 'username or email for dns server credentials',
+ `dns_api_key` varchar(255) NOT NULL COMMENT 'api key or token for the dns server ',
+ `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',
+ `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)',
+ 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;
+
+-- DNS Server Details Table
+CREATE TABLE IF NOT EXISTS `cloud`.`dns_server_details` (
+ `id` bigint unsigned UNIQUE NOT NULL AUTO_INCREMENT COMMENT 'id',
+ `dns_server_id` bigint unsigned NOT NULL COMMENT 'dns_server the detail is related to',
+ `name` varchar(255) NOT NULL COMMENT 'name of the detail',
+ `value` varchar(255) NOT NULL COMMENT 'value of the detail',
+ `display` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Should detail be displayed to the end user',
+ PRIMARY KEY (`id`),
+ CONSTRAINT `fk_dns_server_details__dns_server_id` FOREIGN KEY (`dns_server_id`) REFERENCES `dns_server`(`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- DNS Zone Table (Stores DNS Zone Metadata)
+CREATE TABLE IF NOT EXISTS `cloud`.`dns_zone` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone',
+ `uuid` varchar(40) COMMENT 'uuid of the dns zone',
+ `name` varchar(255) NOT NULL COMMENT 'dns zone name (e.g. example.com)',
+ `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`),
+ 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,
+ 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;
+
+-- DNS Zone Network Map (One-to-Many Link)
+CREATE TABLE IF NOT EXISTS `cloud`.`dns_zone_network_map` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone to network mapping',
+ `uuid` varchar(40),
+ `dns_zone_id` bigint(20) unsigned NOT NULL,
+ `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_network_map__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,
+ CONSTRAINT `fk_dns_map__network_id` FOREIGN KEY (`network_id`) REFERENCES `networks` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
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 00000000000..a1bc1a1141a
--- /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 00000000000..a41e003ae4f
--- /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/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 d77ba27689b..fbf126608ea 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
@@ -144,6 +144,7 @@ SELECT
`nics`.`broadcast_uri` AS `broadcast_uri`,
`nics`.`isolation_uri` AS `isolation_uri`,
`nics`.`enabled` AS `is_nic_enabled`,
+ `nic_details`.`value` AS `nic_dns_name`,
`vpc`.`id` AS `vpc_id`,
`vpc`.`uuid` AS `vpc_uuid`,
`networks`.`uuid` AS `network_uuid`,
@@ -188,7 +189,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`)))
@@ -215,6 +216,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` = 'nicdnsname')))
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/engine/schema/src/main/resources/META-INF/db/views/nic_dns_view.sql b/engine/schema/src/main/resources/META-INF/db/views/nic_dns_view.sql
new file mode 100644
index 00000000000..8c7737db088
--- /dev/null
+++ b/engine/schema/src/main/resources/META-INF/db/views/nic_dns_view.sql
@@ -0,0 +1,40 @@
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements. See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership. The ASF licenses this file
+-- to you under the Apache License, Version 2.0 (the
+-- "License"); you may not use this file except in compliance
+-- with the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing,
+-- software distributed under the License is distributed on an
+-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+-- KIND, either express or implied. See the License for the
+-- specific language governing permissions and limitations
+-- under the License.
+
+-- VIEW `cloud`.`nic_dns_view`;
+
+DROP VIEW IF EXISTS `cloud`.`nic_dns_view`;
+CREATE VIEW `cloud`.`nic_dns_view` AS
+SELECT
+ n.id AS id,
+ n.uuid AS uuid,
+ n.instance_id AS instance_id,
+ n.network_id AS network_id,
+ n.ip4_address AS ip4_address,
+ n.ip6_address AS ip6_address,
+ n.removed AS removed,
+ nd.value AS nic_dns_name,
+ map.dns_zone_id AS dns_zone_id,
+ map.sub_domain AS sub_domain
+FROM
+ `cloud`.`nics` n
+ INNER JOIN
+ `cloud`.`dns_zone_network_map` map ON n.network_id = map.network_id
+ LEFT JOIN
+ `cloud`.`nic_details` nd ON n.id = nd.nic_id AND nd.name = 'nicdnsname'
+WHERE
+ n.instance_id IS NOT NULL AND map.removed IS NULL;
diff --git a/plugins/dns/powerdns/pom.xml b/plugins/dns/powerdns/pom.xml
new file mode 100644
index 00000000000..3edb74f1ce6
--- /dev/null
+++ b/plugins/dns/powerdns/pom.xml
@@ -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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>cloud-plugin-dns-powerdns</artifactId>
+ <name>Apache CloudStack Plugin - PowerDNS</name>
+ <parent>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloudstack-plugins</artifactId>
+ <version>4.23.0.0-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+</project>
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 00000000000..4b28fab17ab
--- /dev/null
+++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java
@@ -0,0 +1,390 @@
+// 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 java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+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.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.HttpPut;
+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.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);
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ 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";
+ public static final String DEFAULT_SERVER_NAME = "localhost";
+
+ private final CloseableHttpClient httpClient;
+
+ 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();
+
+ this.httpClient = HttpClientBuilder.create()
+ .setConnectionManager(connectionManager)
+ .setDefaultRequestConfig(requestConfig)
+ .evictIdleConnections(30, TimeUnit.SECONDS)
+ .disableCookieManagement()
+ .build();
+ }
+
+ 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");
+ }
+ String fallbackId = null;
+ for (JsonNode server : servers) {
+ 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 (fallbackId != null) {
+ return fallbackId;
+ }
+ throw new DnsOperationException("No authoritative PowerDNS server found");
+ }
+
+ public String createZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName,
+ String zoneKind, boolean dnsSecFlag, List<String> nameServers) throws DnsProviderException {
+
+ validateServerId(baseUrl, port, apiKey, externalServerId);
+ 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 + ".");
+ }
+ }
+ 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) {
+ 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 updateZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName,
+ String zoneKind, Boolean dnsSecFlag, List<String> nameServers) throws DnsProviderException {
+
+ 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);
+
+ ObjectNode json = MAPPER.createObjectNode();
+ 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 + ".");
+ }
+ }
+ HttpPut request = new HttpPut(url);
+ request.setEntity(new StringEntity(json.toString(), StandardCharsets.UTF_8));
+ execute(request, apiKey, 204);
+ }
+
+ 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, port, "/servers/" + externalServerId + "/zones/" + encodedZone));
+ execute(request, apiKey, 204, 404);
+ }
+
+ public String modifyRecord(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName,
+ String recordName, String type, long ttl, List<String> contents, String changeType) throws DnsProviderException {
+
+ validateServerId(baseUrl, port, apiKey, externalServerId);
+ 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);
+ }
+ }
+ String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
+ HttpPatch request = new HttpPatch(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone));
+ request.setEntity(new StringEntity(root.toString(), StandardCharsets.UTF_8));
+ execute(request, apiKey, 204);
+ return normalizedRecord.endsWith(".") ? normalizedRecord.substring(0, normalizedRecord.length() - 1) : normalizedRecord;
+ }
+
+ public Iterable<JsonNode> 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, port, "/servers/" + externalServerId + "/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 boolean zoneExists(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName) {
+ try {
+ validateServerId(baseUrl, port, apiKey, externalServerId);
+ String normalizedZone = normalizeZone(zoneName);
+ String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
+ HttpGet request = new HttpGet(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone));
+ execute(request, apiKey, 200);
+ return true;
+ } catch (DnsProviderException | IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ public boolean recordExists(String baseUrl, Integer port, String apiKey,
+ String externalServerId, String zoneName,
+ String recordName, String type) throws DnsProviderException {
+
+ validateServerId(baseUrl, port, apiKey, externalServerId);
+ String normalizedZone = normalizeZone(zoneName);
+ String normalizedRecord = normalizeRecordName(recordName, normalizedZone);
+ String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
+ String urlPath = "/servers/" + externalServerId + "/zones/" + encodedZone +
+ "?rrset_name=" + URLEncoder.encode(normalizedRecord, StandardCharsets.UTF_8) +
+ "&rrset_type=" + type.toUpperCase();
+ HttpGet request = new HttpGet(buildUrl(baseUrl, port, urlPath));
+ JsonNode zoneNode = execute(request, apiKey, 200);
+ if (zoneNode == null || !zoneNode.has(ApiConstants.RR_SETS)) {
+ return false;
+ }
+ JsonNode rrsets = zoneNode.path(ApiConstants.RR_SETS);
+ return rrsets.isArray() && !rrsets.isEmpty();
+ }
+
+ private JsonNode execute(HttpUriRequest request, String apiKey, int... expectedStatus) throws DnsProviderException {
+ request.addHeader(ApiConstants.X_API_KEY, apiKey);
+ request.addHeader("Accept", "application/json");
+ request.addHeader(ApiConstants.CONTENT_TYPE, "application/json");
+
+ try (CloseableHttpResponse response = httpClient.execute(request)) {
+ int status = response.getStatusLine().getStatusCode();
+ String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null;
+
+ for (int expected : expectedStatus) {
+ if (status == expected) {
+ if (body != null && !body.isEmpty()) {
+ return MAPPER.readTree(body);
+ } else {
+ return null;
+ }
+ }
+ }
+ 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);
+ }
+ }
+
+ 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) {
+ if (baseUrl == null) {
+ throw new IllegalArgumentException("Base URL cannot be null");
+ }
+ String url = baseUrl.trim();
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
+ url = "http://" + url;
+ }
+ if (url.endsWith("/")) {
+ url = url.substring(0, url.length() - 1);
+ }
+ return url;
+ }
+
+ 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 + ".";
+ }
+ if (zone.length() < 2) {
+ throw new IllegalArgumentException("Zone name is too short");
+ }
+ return zone;
+ }
+
+ String normalizeRecordName(String recordName, String zoneName) {
+ if (recordName == null) {
+ throw new IllegalArgumentException("Record name must not be null");
+ }
+ String normalizedZone = normalizeZone(zoneName);
+ String name = recordName.trim().toLowerCase();
+ // Apex of the zone
+ if (name.equals("@") || name.isEmpty()) {
+ return normalizedZone;
+ }
+
+ 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;
+ }
+ if (name.contains(".")) {
+ return name + ".";
+ }
+ // Relative name → append zone
+ return name + "." + normalizedZone;
+ }
+
+ @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
new file mode 100644
index 00000000000..9996f705e48
--- /dev/null
+++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java
@@ -0,0 +1,202 @@
+// 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.ArrayList;
+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;
+import org.apache.cloudstack.dns.DnsServer;
+import org.apache.cloudstack.dns.DnsZone;
+import org.apache.cloudstack.dns.exception.DnsProviderException;
+import org.apache.logging.log4j.util.Strings;
+
+import com.cloud.utils.StringUtils;
+import com.cloud.utils.component.AdapterBase;
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class PowerDnsProvider extends AdapterBase implements DnsProvider {
+ private PowerDnsClient client;
+ static String PDNS_SERVER_ID = "pdnsServerId";
+
+ @Override
+ public DnsProviderType getProviderType() {
+ return DnsProviderType.PowerDNS;
+ }
+
+ public void validate(DnsServer server) throws DnsProviderException {
+ validateRequiredServerFields(server);
+ client.validateServerId(server.getUrl(), server.getPort(), server.getDnsApiKey(), server.getDetail(PDNS_SERVER_ID));
+ }
+
+ @Override
+ public String validateAndResolveServer(DnsServer server) throws Exception {
+ validateRequiredServerFields(server);
+ String resolvedDnsServerId = client.resolveServerId(server.getUrl(), server.getPort(), server.getDnsApiKey(), server.getDetail(PDNS_SERVER_ID));
+ if (Strings.isNotBlank(resolvedDnsServerId)) {
+ server.appendDetails(PDNS_SERVER_ID, resolvedDnsServerId);
+ }
+ return resolvedDnsServerId;
+ }
+
+ @Override
+ public String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException {
+ validateRequiredServerAndZoneFields(server, zone);
+ return client.createZone(
+ server.getUrl(),
+ server.getPort(),
+ server.getDnsApiKey(),
+ server.getDetail(PDNS_SERVER_ID),
+ zone.getName(),
+ ApiConstants.NATIVE_ZONE, false, server.getNameServers()
+ );
+ }
+
+ @Override
+ public void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException {
+ validateRequiredServerAndZoneFields(server, zone);
+ client.deleteZone(server.getUrl(), server.getPort(), server.getDnsApiKey(), server.getDetail(PDNS_SERVER_ID), zone.getName());
+ }
+
+ @Override
+ public void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException {
+ validateRequiredServerAndZoneFields(server, zone);
+ client.updateZone(
+ server.getUrl(),
+ server.getPort(),
+ server.getDnsApiKey(),
+ server.getDetail(PDNS_SERVER_ID),
+ zone.getName(), ApiConstants.NATIVE_ZONE, false, server.getNameServers());
+ }
+
+ public enum ChangeType {
+ REPLACE, DELETE
+ }
+
+ @Override
+ public String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException {
+ validateRequiredServerAndZoneFields(server, zone);
+ return applyRecord(
+ server.getUrl(),
+ server.getPort(),
+ server.getDnsApiKey(),
+ server.getDetail(PDNS_SERVER_ID),
+ zone.getName(), record, ChangeType.REPLACE);
+ }
+
+ @Override
+ public String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException {
+ validateRequiredServerAndZoneFields(server, zone);
+ return addRecord(server, zone, record);
+ }
+
+ @Override
+ public String deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException {
+ validateRequiredServerAndZoneFields(server, zone);
+ return applyRecord(server.getUrl(),
+ server.getPort(),
+ server.getDnsApiKey(),
+ server.getDetail(PDNS_SERVER_ID),
+ zone.getName(), record, ChangeType.DELETE);
+ }
+
+ 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<DnsRecord> listRecords(DnsServer server, DnsZone zone) throws DnsProviderException {
+ validateRequiredServerAndZoneFields(server, zone);
+ List<DnsRecord> records = new ArrayList<>();
+ Iterable<JsonNode> rrsetNodes = client.listRecords(server.getUrl(), server.getPort(), server.getDnsApiKey(),
+ server.getDetail(PDNS_SERVER_ID), 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<String> contents = new ArrayList<>();
+ JsonNode recordsNode = rrset.path(ApiConstants.RECORDS);
+ if (recordsNode.isArray()) {
+ for (JsonNode rec : recordsNode) {
+ String content = rec.path(ApiConstants.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;
+ }
+
+ public boolean dnsRecordExists(DnsServer server, DnsZone zone, String recordName, String recordType) throws DnsProviderException {
+ return client.recordExists(server.getUrl(), server.getPort(), server.getDnsApiKey(),
+ server.getDetail(PDNS_SERVER_ID), zone.getName(), recordName, recordType);
+ }
+
+ @Override
+ public boolean dnsZoneExists(DnsServer server, DnsZone zone) {
+ return client.zoneExists(server.getUrl(), server.getPort(), server.getDnsApiKey(), server.getDetail(PDNS_SERVER_ID), zone.getName());
+ }
+
+ void validateRequiredServerAndZoneFields(DnsServer server, DnsZone zone) {
+ validateRequiredServerFields(server);
+ if (StringUtils.isBlank(zone.getName())) {
+ throw new IllegalArgumentException("Zone name cannot be empty");
+ }
+ }
+
+ void validateRequiredServerFields(DnsServer server) {
+ if (StringUtils.isBlank(server.getUrl())) {
+ throw new IllegalArgumentException("PowerDNS API URL cannot be empty");
+ }
+ if (StringUtils.isBlank(server.getDnsApiKey())) {
+ throw new IllegalArgumentException("PowerDNS API key cannot be empty");
+ }
+ }
+
+ @Override
+ public boolean configure(String name, Map<String, Object> params) {
+ if (client == null) {
+ client = new PowerDnsClient();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean stop() {
+ if (client != null) {
+ client.close();
+ }
+ return true;
+ }
+}
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 00000000000..baec3fde6a6
--- /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 00000000000..c9e4937dac5
--- /dev/null
+++ b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml
@@ -0,0 +1,31 @@
+<!--
+ 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.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:aop="http://www.springframework.org/schema/aop"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
+ http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
+ http://www.springframework.org/schema/context
+ http://www.springframework.org/schema/context/spring-context-3.0.xsd"
+ >
+
+ <bean id="powerDnsProvider" class="org.apache.cloudstack.dns.powerdns.PowerDnsProvider" />
+</beans>
diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java
new file mode 100644
index 00000000000..a2b571b154f
--- /dev/null
+++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.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 static org.apache.cloudstack.dns.DnsProviderUtil.appendPublicSuffixToZone;
+import static org.apache.cloudstack.dns.DnsProviderUtil.normalizeDomainForDb;
+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 DnsProviderUtilTest {
+ private final String userZoneName;
+ private final String publicSuffix;
+ private final String expectedResult;
+ private final boolean expectException;
+
+ public DnsProviderUtilTest(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<Object[]> 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(normalizeDomainForDb(userZoneName), publicSuffix);
+ }
+ assertEquals(expectedResult, result);
+ }
+ }
+
+ String executeAppendSuffixTest(String zoneName, String domainSuffix) {
+ return appendPublicSuffixToZone(normalizeDomainForDb(zoneName), domainSuffix);
+ }
+}
diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/NormalizeDnsRecordValueTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/NormalizeDnsRecordValueTest.java
new file mode 100644
index 00000000000..f5bd16a8190
--- /dev/null
+++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/NormalizeDnsRecordValueTest.java
@@ -0,0 +1,197 @@
+// 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.DnsRecord.RecordType.A;
+import static org.apache.cloudstack.dns.DnsRecord.RecordType.AAAA;
+import static org.apache.cloudstack.dns.DnsRecord.RecordType.CNAME;
+import static org.apache.cloudstack.dns.DnsRecord.RecordType.MX;
+import static org.apache.cloudstack.dns.DnsRecord.RecordType.NS;
+import static org.apache.cloudstack.dns.DnsRecord.RecordType.PTR;
+import static org.apache.cloudstack.dns.DnsRecord.RecordType.SRV;
+import static org.apache.cloudstack.dns.DnsRecord.RecordType.TXT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class NormalizeDnsRecordValueTest {
+
+ private final String description;
+ private final String input;
+ private final DnsRecord.RecordType recordType;
+ private final String expected;
+ private final boolean expectException;
+
+ public NormalizeDnsRecordValueTest(String description, String input,
+ DnsRecord.RecordType recordType,
+ String expected, boolean expectException) {
+ this.description = description;
+ this.input = input;
+ this.recordType = recordType;
+ this.expected = expected;
+ this.expectException = expectException;
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][] {
+
+ // ----------------------------------------------------------------
+ // Guard: blank/null value — all record types should throw
+ // ----------------------------------------------------------------
+ {"null value, A record", null, A, null, true},
+ {"empty value, A record", "", A, null, true},
+ {"blank value, A record", " ", A, null, true},
+
+ {"null value, AAAA record", null, AAAA, null, true},
+ {"null value, CNAME record", null, CNAME, null, true},
+ {"null value, MX record", null, MX, null, true},
+ {"null value, TXT record", null, TXT, null, true},
+ {"null value, SRV record", null, SRV, null, true},
+ {"null value, NS record", null, NS, null, true},
+ {"null value, PTR record", null, PTR, null, true},
+
+ // ----------------------------------------------------------------
+ // A record
+ // ----------------------------------------------------------------
+ {"A: valid IPv4", "93.184.216.34", A, "93.184.216.34", false},
+ {"A: valid IPv4 with whitespace", " 93.184.216.34 ", A, "93.184.216.34", false},
+ {"A: loopback", "127.0.0.1", A, "127.0.0.1", false},
+ {"A: all-zeros", "0.0.0.0", A, "0.0.0.0", false},
+ {"A: broadcast", "255.255.255.255", A, "255.255.255.255", false},
+ {"A: private 10.x", "10.0.0.1", A, "10.0.0.1", false},
+ {"A: private 192.168.x", "192.168.1.1", A, "192.168.1.1", false},
+
+ {"A: IPv6 rejected", "2001:db8::1", A, null, true},
+ {"A: domain rejected", "example.com", A, null, true},
+ {"A: partial IP rejected", "192.168.1", A, null, true},
+ {"A: trailing dot rejected", "93.184.216.34.", A, null, true},
+ {"A: octet out of range rejected", "256.0.0.1", A, null, true},
+
+ // ----------------------------------------------------------------
+ // AAAA record
+ // ----------------------------------------------------------------
+ {"AAAA: full IPv6", "2001:0db8:0000:0000:0000:0000:0000:0001", AAAA,
+ "2001:0db8:0000:0000:0000:0000:0000:0001", false},
+
+ {"AAAA: compressed IPv6", "2001:db8::1", AAAA, "2001:db8::1", false},
+ {"AAAA: loopback", "::1", AAAA, "::1", false},
+ {"AAAA: all zeros", "::", AAAA, "::", false},
+ {"AAAA: whitespace", " 2001:db8::1 ", AAAA, "2001:db8::1", false},
+
+ {"AAAA: IPv4 rejected", "93.184.216.34", AAAA, null, true},
+ {"AAAA: domain rejected", "example.com", AAAA, null, true},
+ {"AAAA: invalid hex rejected", "2001:db8::xyz", AAAA, null, true},
+
+ // ----------------------------------------------------------------
+ // CNAME record
+ // ----------------------------------------------------------------
+ {"CNAME: basic", "target.example.com", CNAME, "target.example.com.", false},
+ {"CNAME: uppercase", "TARGET.EXAMPLE.COM", CNAME, "target.example.com.", false},
+ {"CNAME: trailing dot", "target.example.com.", CNAME, "target.example.com.", false},
+ {"CNAME: whitespace", " target.example.com ", CNAME, "target.example.com.", false},
+ {"CNAME: subdomain", "sub.target.example.com", CNAME, "sub.target.example.com.", false},
+
+ {"CNAME: IP rejected", "192.168.1.1", CNAME, null, true},
+ {"CNAME: invalid label", "-bad.example.com", CNAME, null, true},
+
+ // ----------------------------------------------------------------
+ // NS record
+ // ----------------------------------------------------------------
+ {"NS: basic", "ns1.example.com", NS, "ns1.example.com.", false},
+ {"NS: uppercase", "NS1.EXAMPLE.COM", NS, "ns1.example.com.", false},
+ {"NS: trailing dot", "ns1.example.com.", NS, "ns1.example.com.", false},
+ {"NS: subdomain", "ns1.sub.example.com", NS, "ns1.sub.example.com.", false},
+
+ {"NS: IP rejected", "8.8.8.8", NS, null, true},
+ {"NS: invalid label", "ns1-.example.com", NS, null, true},
+
+ // ----------------------------------------------------------------
+ // PTR record
+ // ----------------------------------------------------------------
+ {"PTR: basic", "host.example.com", PTR, "host.example.com.", false},
+ {"PTR: in-addr.arpa", "1.168.192.in-addr.arpa", PTR, "1.168.192.in-addr.arpa.", false},
+ {"PTR: uppercase", "HOST.EXAMPLE.COM", PTR, "host.example.com.", false},
+ {"PTR: trailing dot", "host.example.com.", PTR, "host.example.com.", false},
+
+ {"PTR: IP rejected", "192.168.1.1", PTR, null, true},
+ {"PTR: invalid label", "-host.example.com", PTR, null, true},
+
+ // ----------------------------------------------------------------
+ // MX record
+ // ----------------------------------------------------------------
+ {"MX: standard", "10 mail.example.com", MX, "10 mail.example.com.", false},
+ {"MX: zero priority", "0 mail.example.com", MX, "0 mail.example.com.", false},
+ {"MX: max priority", "65535 mail.example.com", MX, "65535 mail.example.com.", false},
+ {"MX: uppercase", "10 MAIL.EXAMPLE.COM", MX, "10 mail.example.com.", false},
+ {"MX: trailing dot", "10 mail.example.com.", MX, "10 mail.example.com.", false},
+ {"MX: extra whitespace", "10 mail.example.com", MX, "10 mail.example.com.", false},
+
+ {"MX: missing domain", "10", MX, null, true},
+ {"MX: priority out of range", "65536 mail.example.com", MX, null, true},
+ {"MX: non-numeric priority", "abc mail.example.com", MX, null, true},
+ {"MX: IP rejected", "10 192.168.1.1", MX, null, true},
+
+ // ----------------------------------------------------------------
+ // SRV record
+ // ----------------------------------------------------------------
+ {"SRV: standard", "10 20 443 target.example.com", SRV, "10 20 443 target.example.com.", false},
+ {"SRV: zeros", "0 0 1 target.example.com", SRV, "0 0 1 target.example.com.", false},
+ {"SRV: max values", "65535 65535 65535 target.example.com", SRV, "65535 65535 65535 target.example.com.", false},
+ {"SRV: uppercase", "10 20 443 TARGET.EXAMPLE.COM", SRV, "10 20 443 target.example.com.", false},
+ {"SRV: trailing dot", "10 20 443 target.example.com.", SRV, "10 20 443 target.example.com.", false},
+
+ {"SRV: missing target", "10 20 443", SRV, null, true},
+ {"SRV: port 0", "10 20 0 target.example.com", SRV, null, true},
+ {"SRV: priority out of range", "65536 20 443 target.example.com", SRV, null, true},
+ {"SRV: IP rejected", "10 20 443 192.168.1.1", SRV, null, true},
+
+ // ----------------------------------------------------------------
+ // TXT record
+ // ----------------------------------------------------------------
+ {"TXT: trim", " hello world ", TXT, "hello world", false},
+ {"TXT: already clean", "v=spf1 include:example.com ~all", TXT, "v=spf1 include:example.com ~all", false},
+ {"TXT: special chars", "v=DKIM1; k=rsa; p=MIGf", TXT, "v=DKIM1; k=rsa; p=MIGf", false},
+ {"TXT: unicode", "héllo wörld", TXT, "héllo wörld", false},
+ {"TXT: multiple spaces", "key=value with spaces", TXT, "key=value with spaces", false},
+ {"TXT: quoted", "\"quoted value\"", TXT, "\"quoted value\"", false},
+ {"TXT: blank", " ", TXT, null, true},
+ {"TXT: newline", "\n", TXT, null, true},
+ });
+ }
+
+ @Test
+ public void testNormalizeDnsRecordValue() {
+ if (expectException) {
+ try {
+ DnsProviderUtil.normalizeDnsRecordValue(input, recordType);
+ fail("Expected IllegalArgumentException for [" + description + "] input='" + input + "'");
+ } catch (IllegalArgumentException ignored) {}
+ } else {
+ String result = DnsProviderUtil.normalizeDnsRecordValue(input, recordType);
+ assertEquals(description, expected, result);
+ }
+ }
+}
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 00000000000..5b55368770b
--- /dev/null
+++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java
@@ -0,0 +1,432 @@
+// 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 static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import org.apache.cloudstack.dns.exception.DnsOperationException;
+import org.apache.cloudstack.dns.exception.DnsAuthenticationException;
+import org.apache.cloudstack.dns.exception.DnsConflictException;
+import org.apache.cloudstack.dns.exception.DnsNotFoundException;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+@RunWith(MockitoJUnitRunner.class)
+public class PowerDnsClientTest {
+
+ PowerDnsClient client;
+ CloseableHttpClient httpClientMock;
+
+ @Before
+ public void setUp() {
+ client = new PowerDnsClient();
+ httpClientMock = mock(CloseableHttpClient.class);
+ ReflectionTestUtils.setField(client, "httpClient", httpClientMock);
+ }
+
+ private CloseableHttpResponse createResponse(int statusCode, String jsonBody) {
+ CloseableHttpResponse responseMock = mock(CloseableHttpResponse.class);
+ StatusLine statusLineMock = mock(StatusLine.class);
+ when(responseMock.getStatusLine()).thenReturn(statusLineMock);
+ when(statusLineMock.getStatusCode()).thenReturn(statusCode);
+
+ if (jsonBody != null) {
+ when(responseMock.getEntity()).thenReturn(new StringEntity(jsonBody, StandardCharsets.UTF_8));
+ }
+
+ return responseMock;
+ }
+
+ private void mockHttpResponse(int statusCode, String jsonBody) throws IOException {
+ CloseableHttpResponse response = createResponse(statusCode, jsonBody);
+ when(httpClientMock.execute(any(HttpUriRequest.class))).thenReturn(response);
+ }
+
+ @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);
+ }
+
+ @Test
+ public void testDiscoverAuthoritativeServerIdSuccess() throws Exception {
+ mockHttpResponse(200, "[{\"id\":\"localhost\", \"daemon_type\":\"authoritative\"}]");
+ String result = client.discoverAuthoritativeServerId("http://pdns:8081", null, "apikey");
+ assertEquals("localhost", result);
+ }
+
+ @Test
+ public void testDiscoverAuthoritativeServerIdFallback() throws Exception {
+ mockHttpResponse(200, "[{\"id\":\"server1\", \"daemon_type\":\"recursor\"}, {\"id\":\"server2\", \"daemon_type\":\"authoritative\"}]");
+ String result = client.discoverAuthoritativeServerId("http://pdns", 8081, "apikey");
+ assertEquals("server2", result);
+ }
+
+ @Test(expected = DnsOperationException.class)
+ public void testDiscoverAuthoritativeServerIdEmpty() throws Exception {
+ mockHttpResponse(200, "[]");
+ client.discoverAuthoritativeServerId("http://pdns", 8081, "apikey");
+ }
+
+ @Test(expected = DnsOperationException.class)
+ public void testDiscoverAuthoritativeServerIdNoAuthoritative() throws Exception {
+ mockHttpResponse(200, "[{\"id\":\"server1\", \"daemon_type\":\"recursor\"}]");
+ client.discoverAuthoritativeServerId("http://pdns", 8081, "apikey");
+ }
+
+ @Test
+ public void testValidateServerIdSuccess() throws Exception {
+ mockHttpResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}");
+ String result = client.validateServerId("http://pdns", 8081, "apikey", "abc");
+ assertEquals("abc", result);
+ }
+
+ @Test(expected = DnsOperationException.class)
+ public void testValidateServerIdNotAuthoritative() throws Exception {
+ mockHttpResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"recursor\"}");
+ client.validateServerId("http://pdns", 8081, "apikey", "abc");
+ }
+
+ @Test
+ public void testResolveServerIdWithExternalId() throws Exception {
+ mockHttpResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}");
+ String result = client.resolveServerId("http://pdns", 8081, "apikey", "abc");
+ assertEquals("abc", result);
+ }
+
+ @Test
+ public void testResolveServerIdWithoutExternalId() throws Exception {
+ mockHttpResponse(200, "[{\"id\":\"localhost\", \"daemon_type\":\"authoritative\"}]");
+ String result = client.resolveServerId("http://pdns", 8081, "apikey", null);
+ assertEquals("localhost", result);
+ }
+
+ @Test
+ public void testCreateZone() throws Exception {
+ when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer<CloseableHttpResponse>() {
+ @Override
+ public CloseableHttpResponse answer(InvocationOnMock invocation) {
+ HttpUriRequest request = invocation.getArgument(0);
+ if (request.getMethod().equals("GET")) {
+ return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}");
+ }
+ if (request.getMethod().equals("POST")) {
+ return createResponse(201, "{\"id\":\"example.com.\"}");
+ }
+ return createResponse(500, null);
+ }
+ });
+
+ String result = client.createZone("http://pdns", 8081, "apikey", "abc", "example.com", "Native", false, Arrays.asList("ns1.com"));
+ assertEquals("example.com.", result);
+ }
+
+ @Test
+ public void testUpdateZone() throws Exception {
+ when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer<CloseableHttpResponse>() {
+ @Override
+ public CloseableHttpResponse answer(InvocationOnMock invocation) {
+ HttpUriRequest request = invocation.getArgument(0);
+ if (request.getMethod().equals("GET")) {
+ return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}");
+ }
+ if (request.getMethod().equals("PUT")) {
+ return createResponse(204, null);
+ }
+ return createResponse(500, null);
+ }
+ });
+
+ client.updateZone("http://pdns", 8081, "apikey", "abc", "example.com", "Native", true, Arrays.asList("ns1.com"));
+ // No exception means success
+ }
+
+ @Test
+ public void testDeleteZone() throws Exception {
+ when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer<CloseableHttpResponse>() {
+ @Override
+ public CloseableHttpResponse answer(InvocationOnMock invocation) {
+ HttpUriRequest request = invocation.getArgument(0);
+ if (request.getMethod().equals("GET")) {
+ return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}");
+ }
+ if (request.getMethod().equals("DELETE")) {
+ return createResponse(204, null);
+ }
+ return createResponse(500, null);
+ }
+ });
+
+ client.deleteZone("http://pdns", 8081, "apikey", "abc", "example.com");
+ }
+
+ @Test
+ public void testModifyRecord() throws Exception {
+ when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer<CloseableHttpResponse>() {
+ @Override
+ public CloseableHttpResponse answer(InvocationOnMock invocation) {
+ HttpUriRequest request = invocation.getArgument(0);
+ if (request.getMethod().equals("GET")) {
+ return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}");
+ }
+ if (request.getMethod().equals("PATCH")) {
+ return createResponse(204, null);
+ }
+ return createResponse(500, null);
+ }
+ });
+
+ String result = client.modifyRecord("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A", 300, Arrays.asList("1.2.3.4"), "REPLACE");
+ assertEquals("www.example.com", result);
+ }
+
+ @Test
+ public void testModifyRecordApex() throws Exception {
+ when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer<CloseableHttpResponse>() {
+ @Override
+ public CloseableHttpResponse answer(InvocationOnMock invocation) {
+ HttpUriRequest request = invocation.getArgument(0);
+ if (request.getMethod().equals("GET")) {
+ return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}");
+ }
+ if (request.getMethod().equals("PATCH")) {
+ return createResponse(204, null);
+ }
+ return createResponse(500, null);
+ }
+ });
+
+ String result = client.modifyRecord("http://pdns", 8081, "apikey", "abc", "example.com", "@", "A", 300, Arrays.asList("1.2.3.4"), "REPLACE");
+ assertEquals("example.com", result);
+ }
+
+ @Test
+ public void testListRecords() throws Exception {
+ when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer<CloseableHttpResponse>() {
+ @Override
+ public CloseableHttpResponse answer(InvocationOnMock invocation) {
+ HttpUriRequest request = invocation.getArgument(0);
+ // validateServerId uses /servers/abc
+ // listRecords uses /servers/abc/zones/example.com.
+ if (request.getURI().getPath().endsWith("abc")) {
+ return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}");
+ }
+ if (request.getURI().getPath().endsWith("example.com.")) {
+ return createResponse(200, "{\"rrsets\":[{\"name\":\"www.example.com.\",\"type\":\"A\"}]}");
+ }
+ return createResponse(500, null);
+ }
+ });
+
+ Iterable<JsonNode> records = client.listRecords("http://pdns", 8081, "apikey", "abc", "example.com");
+ assertNotNull(records);
+ assertTrue(records.iterator().hasNext());
+ assertEquals("www.example.com.", records.iterator().next().path("name").asText());
+ }
+
+ @Test
+ public void testListRecordsEmpty() throws Exception {
+ when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer<CloseableHttpResponse>() {
+ @Override
+ public CloseableHttpResponse answer(InvocationOnMock invocation) {
+ HttpUriRequest request = invocation.getArgument(0);
+ if (request.getURI().getPath().endsWith("abc")) {
+ return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}");
+ }
+ if (request.getURI().getPath().endsWith("example.com.")) {
+ return createResponse(200, "{}");
+ }
+ return createResponse(500, null);
+ }
+ });
+
+ Iterable<JsonNode> records = client.listRecords("http://pdns", 8081, "apikey", "abc", "example.com");
+ assertNotNull(records);
+ assertTrue(!records.iterator().hasNext());
+ }
+
+ @Test(expected = DnsNotFoundException.class)
+ public void testExecuteThrowsNotFound() throws Exception {
+ mockHttpResponse(404, "Not Found");
+ client.validateServerId("http://pdns", 8081, "apikey", "abc");
+ }
+
+ @Test(expected = DnsAuthenticationException.class)
+ public void testExecuteThrowsAuthError() throws Exception {
+ mockHttpResponse(401, "Unauthorized");
+ client.validateServerId("http://pdns", 8081, "apikey", "abc");
+ }
+
+ @Test(expected = DnsConflictException.class)
+ public void testExecuteThrowsConflictError() throws Exception {
+ mockHttpResponse(409, "Conflict");
+ client.validateServerId("http://pdns", 8081, "apikey", "abc");
+ }
+
+ @Test(expected = DnsOperationException.class)
+ public void testExecuteThrowsUnexpectedStatus() throws Exception {
+ mockHttpResponse(500, "Server Error");
+ client.validateServerId("http://pdns", 8081, "apikey", "abc");
+ }
+ // Route helper: GET /servers/abc → validate; GET /zones/... → zone response
+ private void mockRecordExists(String zoneJson) throws IOException {
+ when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer<CloseableHttpResponse>() {
+ @Override
+ public CloseableHttpResponse answer(InvocationOnMock invocation) {
+ HttpUriRequest request = invocation.getArgument(0);
+ String path = request.getURI().getPath();
+ if (path.endsWith("/abc")) {
+ return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}");
+ }
+ // zone query (contains /zones/)
+ if (zoneJson == null) {
+ return createResponse(200, null); // empty body → execute() returns null
+ }
+ return createResponse(200, zoneJson);
+ }
+ });
+ }
+
+ @Test
+ public void testRecordExistsZoneNodeNull() throws Exception {
+ // execute() returns null → zoneNode == null → false
+ mockRecordExists(null);
+ boolean result = client.recordExists("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A");
+ assertEquals(false, result);
+ }
+
+ @Test
+ public void testRecordExistsMissingRrSetsField() throws Exception {
+ // response has no "rrsets" key → !zoneNode.has(RR_SETS) → false
+ mockRecordExists("{}");
+ boolean result = client.recordExists("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A");
+ assertEquals(false, result);
+ }
+
+ @Test
+ public void testRecordExistsRrSetsNotArray() throws Exception {
+ // rrsets is a scalar string, not an ArrayNode → isArray() == false → false
+ mockRecordExists("{\"rrsets\":\"not-an-array\"}");
+ boolean result = client.recordExists("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A");
+ assertEquals(false, result);
+ }
+
+ @Test
+ public void testRecordExistsEmptyRrSetsArray() throws Exception {
+ // rrsets is an empty array → isArray() == true && isEmpty() == true → false
+ mockRecordExists("{\"rrsets\":[]}");
+ boolean result = client.recordExists("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A");
+ assertEquals(false, result);
+ }
+
+ @Test
+ public void testRecordExistsNonEmptyRrSetsArray() throws Exception {
+ // rrsets is a non-empty array → isArray() == true && !isEmpty() → true
+ mockRecordExists("{\"rrsets\":[{\"name\":\"www.example.com.\",\"type\":\"A\"}]}");
+ boolean result = client.recordExists("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A");
+ assertEquals(true, result);
+ }
+
+ @Test
+ public void testCloseSucceeds() throws Exception {
+ // httpClient.close() completes normally → no exception propagated
+ CloseableHttpClient mockClient = mock(CloseableHttpClient.class);
+ ReflectionTestUtils.setField(client, "httpClient", mockClient);
+ client.close();
+ org.mockito.Mockito.verify(mockClient).close();
+ }
+
+ @Test
+ public void testCloseSwallowsIOException() throws Exception {
+ // httpClient.close() throws IOException → caught and logged (warn), no rethrow
+ CloseableHttpClient mockClient = mock(CloseableHttpClient.class);
+ org.mockito.Mockito.doThrow(new IOException("connection reset")).when(mockClient).close();
+ ReflectionTestUtils.setField(client, "httpClient", mockClient);
+ client.close(); // must NOT throw
+ }
+}
diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsProviderTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsProviderTest.java
new file mode 100644
index 00000000000..eae1c986714
--- /dev/null
+++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsProviderTest.java
@@ -0,0 +1,391 @@
+// 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 static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+import org.apache.cloudstack.dns.DnsRecord;
+import org.apache.cloudstack.dns.DnsRecord.RecordType;
+import org.apache.cloudstack.dns.DnsServer;
+import org.apache.cloudstack.dns.DnsZone;
+import org.apache.cloudstack.dns.DnsProviderType;
+import org.apache.cloudstack.dns.exception.DnsProviderException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+@RunWith(MockitoJUnitRunner.class)
+public class PowerDnsProviderTest {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private PowerDnsProvider provider;
+ private PowerDnsClient clientMock;
+ private DnsServer serverMock;
+ private DnsZone zoneMock;
+
+ @Before
+ public void setUp() {
+ provider = new PowerDnsProvider();
+ clientMock = mock(PowerDnsClient.class);
+ serverMock = mock(DnsServer.class);
+ zoneMock = mock(DnsZone.class);
+ ReflectionTestUtils.setField(provider, "client", clientMock);
+
+ when(serverMock.getUrl()).thenReturn("http://pdns:8081");
+ when(serverMock.getDnsApiKey()).thenReturn("secret");
+ when(serverMock.getPort()).thenReturn(8081);
+ when(serverMock.getDetail(PowerDnsProvider.PDNS_SERVER_ID)).thenReturn("localhost");
+ when(serverMock.getNameServers()).thenReturn(Arrays.asList("ns1.example.com"));
+
+ when(zoneMock.getName()).thenReturn("example.com");
+ }
+
+ @Test
+ public void testGetProviderType() {
+ assertEquals(DnsProviderType.PowerDNS, provider.getProviderType());
+ }
+
+ @Test
+ public void testConfigureCreatesClientWhenNull() {
+ PowerDnsProvider freshProvider = new PowerDnsProvider();
+ boolean result = freshProvider.configure("test", new HashMap<>());
+ assertTrue(result);
+ assertNotNull(ReflectionTestUtils.getField(freshProvider, "client"));
+ }
+
+ @Test
+ public void testConfigureDoesNotReplaceExistingClient() {
+ PowerDnsClient existingClient = mock(PowerDnsClient.class);
+ ReflectionTestUtils.setField(provider, "client", existingClient);
+
+ boolean result = provider.configure("test", new HashMap<>());
+
+ assertTrue(result);
+ assertEquals(existingClient, ReflectionTestUtils.getField(provider, "client"));
+ }
+
+ @Test
+ public void testStopClosesClient() {
+ boolean result = provider.stop();
+ assertTrue(result);
+ verify(clientMock, times(1)).close();
+ }
+
+ @Test
+ public void testStopWithNullClientSucceeds() {
+ ReflectionTestUtils.setField(provider, "client", null);
+ boolean result = provider.stop();
+ assertTrue(result);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testValidateServerFieldsNullUrl() {
+ when(serverMock.getUrl()).thenReturn(null);
+ provider.validateRequiredServerFields(serverMock);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testValidateServerFieldsBlankUrl() {
+ when(serverMock.getUrl()).thenReturn(" ");
+ provider.validateRequiredServerFields(serverMock);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testValidateServerFieldsNullApiKey() {
+ when(serverMock.getDnsApiKey()).thenReturn(null);
+ provider.validateRequiredServerFields(serverMock);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testValidateServerFieldsBlankApiKey() {
+ when(serverMock.getDnsApiKey()).thenReturn("");
+ provider.validateRequiredServerFields(serverMock);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testValidateServerAndZoneFieldsBlankZoneName() {
+ when(zoneMock.getName()).thenReturn(" ");
+ provider.validateRequiredServerAndZoneFields(serverMock, zoneMock);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testValidateServerAndZoneFieldsNullZoneName() {
+ when(zoneMock.getName()).thenReturn(null);
+ provider.validateRequiredServerAndZoneFields(serverMock, zoneMock);
+ }
+
+ @Test
+ public void testValidateDelegatesToClient() throws DnsProviderException {
+ when(clientMock.validateServerId(anyString(), anyInt(), anyString(), anyString())).thenReturn("localhost");
+ provider.validate(serverMock);
+ verify(clientMock).validateServerId("http://pdns:8081", 8081, "secret", "localhost");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testValidateThrowsWhenServerUrlBlank() throws DnsProviderException {
+ when(serverMock.getUrl()).thenReturn("");
+ provider.validate(serverMock);
+ }
+
+ @Test
+ public void testValidateAndResolveServer() throws Exception {
+ when(clientMock.resolveServerId(anyString(), anyInt(), anyString(), anyString())).thenReturn("localhost");
+ String result = provider.validateAndResolveServer(serverMock);
+ assertEquals("localhost", result);
+ verify(clientMock).resolveServerId("http://pdns:8081", 8081, "secret", "localhost");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testValidateAndResolveServerThrowsWhenUrlBlank() throws Exception {
+ when(serverMock.getUrl()).thenReturn(null);
+ provider.validateAndResolveServer(serverMock);
+ }
+
+ @Test
+ public void testProvisionZoneDelegatesToClient() throws DnsProviderException {
+ when(clientMock.createZone(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(), eq(false), anyList())).thenReturn("example.com.");
+ String zoneId = provider.provisionZone(serverMock, zoneMock);
+ assertEquals("example.com.", zoneId);
+ verify(clientMock).createZone("http://pdns:8081", 8081, "secret", "localhost", "example.com",
+ "Native", false, Arrays.asList("ns1.example.com"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testProvisionZoneThrowsWhenZoneNameBlank() throws DnsProviderException {
+ when(zoneMock.getName()).thenReturn(null);
+ provider.provisionZone(serverMock, zoneMock);
+ }
+
+ @Test
+ public void testDeleteZoneDelegatesToClient() throws DnsProviderException {
+ provider.deleteZone(serverMock, zoneMock);
+ verify(clientMock).deleteZone("http://pdns:8081", 8081, "secret", "localhost", "example.com");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testDeleteZoneThrowsWhenZoneNameBlank() throws DnsProviderException {
+ when(zoneMock.getName()).thenReturn("");
+ provider.deleteZone(serverMock, zoneMock);
+ }
+
+ @Test
+ public void testUpdateZoneDelegatesToClient() throws DnsProviderException {
+ provider.updateZone(serverMock, zoneMock);
+ verify(clientMock).updateZone("http://pdns:8081", 8081, "secret", "localhost", "example.com",
+ "Native", false, Arrays.asList("ns1.example.com"));
+ }
+
+ @Test
+ public void testAddRecordDelegatesToClient() throws DnsProviderException {
+ DnsRecord record = new DnsRecord("www", RecordType.A, Arrays.asList("1.2.3.4"), 300);
+ when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(),
+ anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("www.example.com");
+
+ String result = provider.addRecord(serverMock, zoneMock, record);
+
+ assertEquals("www.example.com", result);
+ verify(clientMock).modifyRecord("http://pdns:8081", 8081, "secret", "localhost", "example.com",
+ "www", "A", 300L, Arrays.asList("1.2.3.4"), "REPLACE");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddRecordThrowsWhenServerUrlBlank() throws DnsProviderException {
+ when(serverMock.getUrl()).thenReturn("");
+ DnsRecord record = new DnsRecord("www", RecordType.A, Arrays.asList("1.2.3.4"), 300);
+ provider.addRecord(serverMock, zoneMock, record);
+ }
+
+ @Test
+ public void testUpdateRecordDelegatesToAddRecord() throws DnsProviderException {
+ DnsRecord record = new DnsRecord("mail", RecordType.MX, Arrays.asList("10 mail.example.com"), 300);
+ when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(),
+ anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("mail.example.com");
+
+ String result = provider.updateRecord(serverMock, zoneMock, record);
+
+ assertEquals("mail.example.com", result);
+ verify(clientMock).modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(),
+ eq("mail"), eq("MX"), eq(300L), eq(Arrays.asList("10 mail.example.com")), eq("REPLACE"));
+ }
+
+ @Test
+ public void testDeleteRecordDelegatesToClientWithDeleteChangeType() throws DnsProviderException {
+ DnsRecord record = new DnsRecord("old", RecordType.CNAME, Arrays.asList("target.com"), 600);
+ when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(),
+ anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("old.example.com");
+
+ String result = provider.deleteRecord(serverMock, zoneMock, record);
+
+ assertEquals("old.example.com", result);
+ verify(clientMock).modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(),
+ eq("old"), eq("CNAME"), eq(600L), eq(Arrays.asList("target.com")), eq("DELETE"));
+ }
+
+ @Test
+ public void testApplyRecordPassesChangeTypeToClient() throws DnsProviderException {
+ DnsRecord record = new DnsRecord("txt", RecordType.TXT, Arrays.asList("v=spf1 include:example.com ~all"), 3600);
+ when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(),
+ anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("txt.example.com");
+
+ provider.applyRecord("http://pdns:8081", 8081, "secret", "localhost", "example.com",
+ record, PowerDnsProvider.ChangeType.REPLACE);
+
+ verify(clientMock).modifyRecord("http://pdns:8081", 8081, "secret", "localhost", "example.com",
+ "txt", "TXT", 3600L, Arrays.asList("v=spf1 include:example.com ~all"), "REPLACE");
+ }
+
+ @Test
+ public void testListRecordsParsesRrsets() throws DnsProviderException {
+ ObjectNode aRecord = MAPPER.createObjectNode();
+ aRecord.put("name", "www.example.com.");
+ aRecord.put("type", "A");
+ aRecord.put("ttl", 300);
+ ArrayNode records = aRecord.putArray("records");
+ records.addObject().put("content", "1.2.3.4");
+
+ ObjectNode mxRecord = MAPPER.createObjectNode();
+ mxRecord.put("name", "example.com.");
+ mxRecord.put("type", "MX");
+ mxRecord.put("ttl", 600);
+ ArrayNode mxRecords = mxRecord.putArray("records");
+ mxRecords.addObject().put("content", "10 mail.example.com");
+
+ when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString()))
+ .thenReturn(Arrays.asList(aRecord, mxRecord));
+
+ List<DnsRecord> result = provider.listRecords(serverMock, zoneMock);
+
+ assertEquals(2, result.size());
+
+ DnsRecord first = result.get(0);
+ assertEquals("www.example.com.", first.getName());
+ assertEquals(RecordType.A, first.getType());
+ assertEquals(300, first.getTtl());
+ assertEquals(Arrays.asList("1.2.3.4"), first.getContents());
+
+ DnsRecord second = result.get(1);
+ assertEquals("example.com.", second.getName());
+ assertEquals(RecordType.MX, second.getType());
+ assertEquals(600, second.getTtl());
+ assertEquals(Arrays.asList("10 mail.example.com"), second.getContents());
+ }
+
+ @Test
+ public void testListRecordsSkipsSoaRecords() throws DnsProviderException {
+ ObjectNode soaRecord = MAPPER.createObjectNode();
+ soaRecord.put("name", "example.com.");
+ soaRecord.put("type", "SOA");
+ soaRecord.put("ttl", 3600);
+ soaRecord.putArray("records").addObject().put("content", "ns1.example.com. admin.example.com. ...");
+
+ when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString()))
+ .thenReturn(Collections.singletonList(soaRecord));
+
+ List<DnsRecord> result = provider.listRecords(serverMock, zoneMock);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void testListRecordsSkipsUnknownRecordTypes() throws DnsProviderException {
+ ObjectNode unknownRecord = MAPPER.createObjectNode();
+ unknownRecord.put("name", "test.example.com.");
+ unknownRecord.put("type", "UNKNOWNTYPE");
+ unknownRecord.put("ttl", 300);
+ unknownRecord.putArray("records").addObject().put("content", "some-data");
+
+ when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString()))
+ .thenReturn(Collections.singletonList(unknownRecord));
+
+ List<DnsRecord> result = provider.listRecords(serverMock, zoneMock);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void testListRecordsIgnoresEmptyContentEntries() throws DnsProviderException {
+ ObjectNode aRecord = MAPPER.createObjectNode();
+ aRecord.put("name", "host.example.com.");
+ aRecord.put("type", "A");
+ aRecord.put("ttl", 300);
+ ArrayNode records = aRecord.putArray("records");
+ records.addObject().put("content", "");
+ records.addObject().put("content", "5.6.7.8");
+
+ when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString()))
+ .thenReturn(Collections.singletonList(aRecord));
+
+ List<DnsRecord> result = provider.listRecords(serverMock, zoneMock);
+ assertEquals(1, result.size());
+ assertEquals(Collections.singletonList("5.6.7.8"), result.get(0).getContents());
+ }
+
+ @Test
+ public void testListRecordsReturnsEmptyListWhenClientReturnsEmpty() throws DnsProviderException {
+ when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString()))
+ .thenReturn(Collections.emptyList());
+
+ List<DnsRecord> result = provider.listRecords(serverMock, zoneMock);
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test(expected = DnsProviderException.class)
+ public void testListRecordsPropagatesClientException() throws DnsProviderException {
+ when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString()))
+ .thenThrow(mock(DnsProviderException.class));
+
+ provider.listRecords(serverMock, zoneMock);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testListRecordsThrowsWhenZoneNameBlank() throws DnsProviderException {
+ when(zoneMock.getName()).thenReturn("");
+ provider.listRecords(serverMock, zoneMock);
+ }
+
+ @Test
+ public void testChangeTypeValues() {
+ assertEquals("REPLACE", PowerDnsProvider.ChangeType.REPLACE.name());
+ assertEquals("DELETE", PowerDnsProvider.ChangeType.DELETE.name());
+ }
+}
diff --git a/plugins/pom.xml b/plugins/pom.xml
index 3a3b39b7c16..e1e1ba08889 100755
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -156,6 +156,7 @@
<module>user-two-factor-authenticators/totp</module>
<module>user-two-factor-authenticators/static-pin</module>
+ <module>dns/powerdns</module>
</modules>
<dependencies>
diff --git a/server/src/main/java/com/cloud/acl/DomainChecker.java b/server/src/main/java/com/cloud/acl/DomainChecker.java
index 0500960abb1..116df262352 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.RolePermissionEntity;
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,11 @@ public class DomainChecker extends AdapterBase implements SecurityChecker {
_networkMgr.checkRouterPermissions(caller, (VirtualRouter)entity);
} else if (entity instanceof AffinityGroup) {
return false;
- } else {
+ } else if (entity instanceof DnsServer) {
+ dnsProviderManager.checkDnsServerPermission(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/api/ApiDBUtils.java b/server/src/main/java/com/cloud/api/ApiDBUtils.java
index d8b3c155ae8..72463ddb171 100644
--- a/server/src/main/java/com/cloud/api/ApiDBUtils.java
+++ b/server/src/main/java/com/cloud/api/ApiDBUtils.java
@@ -81,6 +81,10 @@ import org.apache.cloudstack.backup.dao.BackupOfferingDao;
import org.apache.cloudstack.backup.dao.BackupRepositoryDao;
import org.apache.cloudstack.backup.dao.BackupScheduleDao;
import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.dns.dao.DnsZoneDao;
+import org.apache.cloudstack.dns.dao.DnsZoneNetworkMapDao;
+import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO;
+import org.apache.cloudstack.dns.vo.DnsZoneVO;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
@@ -364,6 +368,7 @@ import org.apache.cloudstack.acl.dao.ApiKeyPairDao;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.util.Strings;
public class ApiDBUtils {
private static final Logger log = LogManager.getLogger(ApiDBUtils.class);
@@ -505,6 +510,8 @@ public class ApiDBUtils {
static SharedFSJoinDao s_sharedFSJoinDao;
static BucketDao s_bucketDao;
+ static DnsZoneDao s_dnsZoneDao;
+ static DnsZoneNetworkMapDao s_dnsZoneNetworkMapDao;
static VirtualMachineManager s_virtualMachineManager;
@Inject
@@ -777,6 +784,10 @@ public class ApiDBUtils {
private VirtualMachineManager virtualMachineManager;
@Inject
private SharedFSJoinDao sharedFSJoinDao;
+ @Inject
+ private DnsZoneDao dnsZoneDao;
+ @Inject
+ private DnsZoneNetworkMapDao dnsZoneNetworkMapDao;
@PostConstruct
void init() {
@@ -915,6 +926,8 @@ public class ApiDBUtils {
s_bucketDao = bucketDao;
s_virtualMachineManager = virtualMachineManager;
s_sharedFSJoinDao = sharedFSJoinDao;
+ s_dnsZoneDao = dnsZoneDao;
+ s_dnsZoneNetworkMapDao = dnsZoneNetworkMapDao;
}
// ///////////////////////////////////////////////////////////
@@ -2297,6 +2310,17 @@ public class ApiDBUtils {
return details.isEmpty() ? null : details;
}
+ public static Pair<String, String> findDnsZoneByNetworkId(long networkId) {
+ DnsZoneNetworkMapVO dnsNetworkMapVO = s_dnsZoneNetworkMapDao.findByNetworkId(networkId);
+ if (dnsNetworkMapVO != null) {
+ DnsZoneVO dnsZoneVO = s_dnsZoneDao.findById(dnsNetworkMapVO.getDnsZoneId());
+ if (dnsZoneVO != null && Strings.isNotBlank(dnsZoneVO.getName())) {
+ return new Pair<> (dnsZoneVO.getName(), dnsNetworkMapVO.getSubDomain());
+ }
+ }
+ return new Pair<>(null, null);
+ }
+
public static boolean isAdmin(Account account) {
return s_accountService.isAdmin(account.getId());
}
diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
index e27339941f6..7cebeed7140 100644
--- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java
+++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
@@ -549,6 +549,7 @@ public class ApiResponseHelper implements ResponseGenerator, ResourceIdSupport {
@Inject
AsyncJobDao asyncJobDao;
+
public static String getPrettyDomainPath(String path) {
if (path == null) {
return null;
@@ -2657,6 +2658,14 @@ public class ApiResponseHelper implements ResponseGenerator, ResourceIdSupport {
response.setDetails(details);
}
+ Pair<String, String> dnsZoneAndSubDomain = ApiDBUtils.findDnsZoneByNetworkId(network.getId());
+ if (StringUtils.isNotBlank(dnsZoneAndSubDomain.first())) {
+ response.setDnsZone(dnsZoneAndSubDomain.first());
+ }
+ if (StringUtils.isNotBlank(dnsZoneAndSubDomain.second())) {
+ response.setDnsSubdomain(dnsZoneAndSubDomain.second());
+ }
+
DataCenter zone = ApiDBUtils.findZoneById(network.getDataCenterId());
if (zone != null) {
response.setZoneId(zone.getUuid());
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 aeb54de1290..3f7f459c2a1 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
@@ -394,6 +394,7 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
nicResponse.setIp6Gateway(userVm.getIp6Gateway());
nicResponse.setIp6Cidr(userVm.getIp6Cidr());
nicResponse.setEnabled(userVm.isNicEnabled());
+ nicResponse.setNicDnsName(userVm.getNicDnsName());
if (userVm.getBroadcastUri() != null) {
nicResponse.setBroadcastUri(userVm.getBroadcastUri().toString());
}
@@ -694,6 +695,9 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation<UserVmJo
nicResponse.setIp6Gateway(uvo.getIp6Gateway());
/*13: IPv6Cidr*/
nicResponse.setIp6Cidr(uvo.getIp6Cidr());
+ /* dnsRecordUrl */
+ nicResponse.setNicDnsName(uvo.getNicDnsName());
+
/*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 b697c357115..6f84bd9b1e3 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.Temporal;
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")
@@ -407,6 +408,9 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
@Column(name = "public_ip_address")
private String publicIpAddress;
+ @Column(name = "nic_dns_name")
+ private String nicDnsName;
+
@Column(name = "user_data", updatable = true, nullable = true, length = 2048)
private String userData;
@@ -1110,4 +1114,8 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro
public boolean isNicEnabled() {
return isNicEnabled;
}
+
+ public String getNicDnsName() {
+ return nicDnsName;
+ }
}
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 779d26d51f1..3ad826e8977 100644
--- a/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java
@@ -315,7 +315,7 @@ public class FirewallManagerImpl extends ManagerBase implements FirewallService,
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 6c8d9c842fb..2a49680e8fd 100644
--- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
+++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
@@ -96,6 +96,8 @@ import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
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.dns.DnsZone;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
@@ -778,7 +780,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
}
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 AffinityGroup) && !(entity instanceof VirtualRouter)
+ && !(entity instanceof DnsServer) && !(entity instanceof DnsZone)) {
List<ControlledEntity> 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/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
index e6e110e341f..ea3c584e4b1 100644
--- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
+++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
@@ -16,9 +16,15 @@
// under the License.
package com.cloud.vm;
+import static com.cloud.event.EventTypes.EVENT_NIC_CREATE;
+import static com.cloud.event.EventTypes.EVENT_NIC_DELETE;
+import static com.cloud.event.EventTypes.EVENT_NIC_UPDATE;
+import static com.cloud.event.EventTypes.EVENT_VM_UPDATE;
import static com.cloud.hypervisor.Hypervisor.HypervisorType.Functionality;
import static com.cloud.storage.Volume.IOPS_LIMIT;
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
+import static com.cloud.vm.VirtualMachineManager.Topics.VM_ACTION;
+import static com.cloud.vm.VirtualMachineManager.Topics.VM_LIFECYCLE_STATE;
import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS;
import static org.apache.cloudstack.api.ApiConstants.MIN_IOPS;
@@ -161,6 +167,7 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.util.Strings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
@@ -1464,7 +1471,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
@Override
- @ActionEvent(eventType = EventTypes.EVENT_NIC_CREATE, eventDescription = "Creating NIC", async = true)
+ @ActionEvent(eventType = EVENT_NIC_CREATE, eventDescription = "Creating NIC", async = true)
public UserVm addNicToVirtualMachine(AddNicToVMCmd cmd) throws InvalidParameterValueException, PermissionDeniedException, CloudRuntimeException {
Long vmId = cmd.getVmId();
Long networkId = cmd.getNetworkId();
@@ -1563,6 +1570,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
CallContext.current().putContextParameter(Nic.class, guestNic.getUuid());
logger.debug(String.format("Successful addition of %s from %s through %s", network, vmInstance, guestNic));
+ publishNicEventMessageBus(vmInstance.getId(), vmInstance.getAccountId(), guestNic.getId(), EVENT_NIC_CREATE);
return _vmDao.findById(vmInstance.getId());
}
@@ -1576,6 +1584,55 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
throw new CloudRuntimeException(String.format("Zone %s, has a NetworkType of Basic. Can't add a new NIC to a Instance on a Basic Network", dc));
}
+ private void publishVmLifecycleMessageBus(UserVm instance, @Nullable VirtualMachine.State oldState, VirtualMachine.State newState) {
+ try {
+ Map<String, Object> event = new HashMap<>();
+ event.put(ApiConstants.EVENT_ID, UUID.randomUUID().toString());
+ event.put(ApiConstants.INSTANCE_ID, instance.getId());
+ event.put(ApiConstants.ACCOUNT_ID, instance.getAccountId());
+ event.put(ApiConstants.OLD_STATE, oldState != null ? oldState : State.Unknown);
+ event.put(ApiConstants.NEW_STATE, newState);
+ event.put(ApiConstants.TIME_STAMP, System.currentTimeMillis());
+ messageBus.publish(_name, VM_LIFECYCLE_STATE, PublishScope.GLOBAL, event);
+ } catch (Exception ex) {
+ logger.warn("Failed to publish lifecycle event for Instance: {}", instance.getUuid(), ex);
+ }
+ }
+
+ private void publishNicEventMessageBus(Long instanceId, Long accountId, Long nicId, String eventType) {
+ try {
+ Map<String, Object> event = new HashMap<>();
+ event.put(ApiConstants.EVENT_ID, UUID.randomUUID().toString());
+ event.put(ApiConstants.INSTANCE_ID, instanceId);
+ event.put(ApiConstants.ACCOUNT_ID, accountId);
+ event.put(ApiConstants.NIC_ID, nicId);
+ event.put(ApiConstants.EVENT_TYPE, eventType); // NIC.CREATE, NIC.DELETE or NIC.UPDATE
+ event.put(ApiConstants.TIME_STAMP, System.currentTimeMillis());
+
+ messageBus.publish(_name, Nic.Topics.NIC_LIFECYCLE, PublishScope.GLOBAL, event);
+ } catch (Exception ex) {
+ logger.error("Failed to publish lifecycle event for NIC with ID: {}", nicId, ex);
+ }
+ }
+
+ private void publishVmHostNameUpdateMessageBus(long instanceId, String oldHostName, String hostName) {
+ if (Strings.isBlank(hostName) || oldHostName.equalsIgnoreCase(hostName)) {
+ return;
+ }
+ try {
+ Map<String, Object> event = new HashMap<>();
+ event.put(ApiConstants.EVENT_ID, UUID.randomUUID().toString());
+ event.put(ApiConstants.INSTANCE_ID, instanceId);
+ event.put(ApiConstants.OLD_HOST_NAME, oldHostName);
+ event.put(ApiConstants.HOST_NAME, hostName);
+ event.put(ApiConstants.EVENT_TYPE, EVENT_VM_UPDATE);
+ event.put(ApiConstants.TIME_STAMP, System.currentTimeMillis());
+ messageBus.publish(_name, VM_ACTION, PublishScope.GLOBAL, event);
+ } catch (Exception ex) {
+ logger.error("Failed to publish Instance action event for ID: {}", instanceId, ex);
+ }
+ }
+
/**
* Set NIC as default if VM has no default NIC
* @param vmInstance VM instance to be checked
@@ -1690,6 +1747,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
logger.debug("Successful removal of " + network + " from " + vmInstance);
+ publishNicEventMessageBus(vmInstance.getId(), vmInstance.getAccountId(), nic.getId(), EVENT_NIC_DELETE);
return _vmDao.findById(vmInstance.getId());
}
@@ -1931,7 +1989,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
logger.debug("Updating IPv4 address of NIC " + nicVO + " to " + ipaddr + "/" + nicVO.getIPv4Netmask() + " with gateway " + nicVO.getIPv4Gateway());
nicVO.setIPv4Address(ipaddr);
_nicDao.persist(nicVO);
-
+ publishNicEventMessageBus(vm.getId(), vm.getAccountId(), nicVO.getId(), EVENT_NIC_UPDATE);
return vm;
}
@@ -2974,7 +3032,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
@Override
- @ActionEvent(eventType = EventTypes.EVENT_VM_UPDATE, eventDescription = "updating Vm")
+ @ActionEvent(eventType = EVENT_VM_UPDATE, eventDescription = "updating Vm")
public UserVm updateVirtualMachine(UpdateVMCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException {
validateInputsAndPermissionForUpdateVirtualMachineCommand(cmd);
@@ -3355,6 +3413,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
if (State.Running == vm.getState()) {
+ publishVmHostNameUpdateMessageBus(vm.getId(), vm.getHostName(), hostName);
updateDns(vm, hostName);
}
@@ -3683,7 +3742,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
logger.warn("Tried to destroy ROOT volume for VM [{}], but couldn't retrieve it.", vm);
}
}
-
+ publishVmLifecycleMessageBus(destroyedVm, vm.getState(), VirtualMachine.State.Destroyed);
return destroyedVm;
}
@@ -5436,7 +5495,9 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
additionalParams.put(VirtualMachineProfile.Param.VmPassword, cmd.getPassword());
}
- return startVirtualMachine(vmId, podId, clusterId, hostId, diskOfferingMap, additionalParams, cmd.getDeploymentPlanner());
+ UserVm userVm = startVirtualMachine(vmId, podId, clusterId, hostId, diskOfferingMap, additionalParams, cmd.getDeploymentPlanner());
+ publishVmLifecycleMessageBus(userVm, null, VirtualMachine.State.Running);
+ return userVm;
}
private UserVm startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, Map<Long, DiskOffering> diskOfferingMap
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 00000000000..b451da1baf7
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java
@@ -0,0 +1,1230 @@
+// 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 com.cloud.event.EventTypes.EVENT_NIC_CREATE;
+import static com.cloud.event.EventTypes.EVENT_NIC_DELETE;
+import static com.cloud.event.EventTypes.EVENT_NIC_UPDATE;
+import static com.cloud.event.EventTypes.EVENT_VM_UPDATE;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+import javax.naming.ConfigurationException;
+
+import org.apache.cloudstack.api.ApiCommandResourceType;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd;
+import org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd;
+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;
+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.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.DnsServerDetailsDao;
+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.dao.NicDnsJoinDao;
+import org.apache.cloudstack.dns.exception.DnsConflictException;
+import org.apache.cloudstack.dns.exception.DnsNotFoundException;
+import org.apache.cloudstack.dns.exception.DnsProviderException;
+import org.apache.cloudstack.dns.exception.DnsTransportException;
+import org.apache.cloudstack.dns.vo.DnsServerJoinVO;
+import org.apache.cloudstack.dns.vo.DnsServerVO;
+import org.apache.cloudstack.dns.vo.DnsZoneJoinVO;
+import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO;
+import org.apache.cloudstack.dns.vo.DnsZoneVO;
+import org.apache.cloudstack.dns.vo.NicDnsJoinVO;
+import org.apache.cloudstack.framework.messagebus.MessageBus;
+import org.apache.cloudstack.framework.messagebus.MessageSubscriber;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.BooleanUtils;
+import org.springframework.stereotype.Component;
+
+import com.cloud.domain.Domain;
+import com.cloud.domain.dao.DomainDao;
+import com.cloud.event.ActionEvent;
+import com.cloud.event.ActionEventUtils;
+import com.cloud.event.EventTypes;
+import com.cloud.exception.InvalidParameterValueException;
+import com.cloud.exception.PermissionDeniedException;
+import com.cloud.network.dao.NetworkDao;
+import com.cloud.network.dao.NetworkVO;
+import com.cloud.user.Account;
+import com.cloud.user.AccountManager;
+import com.cloud.user.User;
+import com.cloud.user.dao.AccountDao;
+import com.cloud.utils.Pair;
+import com.cloud.utils.StringUtils;
+import com.cloud.utils.component.ManagerBase;
+import com.cloud.utils.component.PluggableService;
+import com.cloud.utils.db.Filter;
+import com.cloud.utils.db.Transaction;
+import com.cloud.utils.db.TransactionCallback;
+import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn;
+import com.cloud.utils.db.TransactionStatus;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.vm.Nic;
+import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.VirtualMachineManager;
+import com.cloud.vm.dao.NicDao;
+import com.cloud.vm.dao.NicDetailsDao;
+import com.cloud.vm.dao.UserVmDao;
+import com.cloud.vm.dao.VMInstanceDao;
+
+@Component
+public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderManager, PluggableService {
+ List<DnsProvider> dnsProviders;
+ @Inject
+ AccountManager accountMgr;
+ @Inject
+ DnsServerDao dnsServerDao;
+ @Inject
+ DnsZoneDao dnsZoneDao;
+ @Inject
+ NetworkDao networkDao;
+ @Inject
+ DnsZoneNetworkMapDao dnsZoneNetworkMapDao;
+ @Inject
+ UserVmDao userVmDao;
+ @Inject
+ NicDao nicDao;
+ @Inject
+ DomainDao domainDao;
+ @Inject
+ DnsZoneJoinDao dnsZoneJoinDao;
+ @Inject
+ DnsServerJoinDao dnsServerJoinDao;
+ @Inject
+ AccountDao accountDao;
+ @Inject
+ NicDetailsDao nicDetailsDao;
+ @Inject
+ MessageBus messageBus;
+ @Inject
+ VMInstanceDao vmInstanceDao;
+ @Inject
+ NicDnsJoinDao nicDnsJoinDao;
+ @Inject
+ DnsServerDetailsDao dnsServerDetailsDao;
+
+ private static final Set<VirtualMachine.State> VM_ALLOWED_STATES = EnumSet.of(VirtualMachine.State.Running, VirtualMachine.State.Stopped);
+
+ private DnsProvider getProviderByType(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
+ @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());
+ if (existing != null) {
+ throw new InvalidParameterValueException(
+ "This Account already has a DNS server integration for URL: " + cmd.getUrl());
+ }
+
+ boolean isDnsPublic = cmd.isPublic();
+ String publicDomainSuffix = cmd.getPublicDomainSuffix();
+ if (!accountMgr.isRootAdmin(caller.getId()) && !accountMgr.isDomainAdmin(caller.getId())) {
+ logger.info("Only root admin and domain admin users are allowed to configure a public DNS server");
+ isDnsPublic = false;
+ publicDomainSuffix = null;
+ }
+
+ if (StringUtils.isNotBlank(publicDomainSuffix)) {
+ publicDomainSuffix = DnsProviderUtil.normalizeDomainForDb(publicDomainSuffix);
+ }
+
+ DnsProviderType type = cmd.getProvider();
+ DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), cmd.getPort(), type,
+ cmd.getDnsUserName(), cmd.getDnsApiKey(), isDnsPublic, publicDomainSuffix, cmd.getNameServers(),
+ caller.getAccountId(), caller.getDomainId());
+
+ if (MapUtils.isNotEmpty(cmd.getDetails())) {
+ server.setDetails(cmd.getDetails());
+ }
+
+ try {
+ DnsProvider provider = getProviderByType(type);
+ provider.validateAndResolveServer(server);
+ return dnsServerDao.persist(server);
+ } catch (Exception ex) {
+ logger.error("Failed to validate DNS server", ex);
+ throw new CloudRuntimeException("Failed to validate DNS server");
+ }
+ }
+
+ @Override
+ public ListResponse<DnsServerResponse> listDnsServers(ListDnsServersCmd cmd) {
+ Pair<List<DnsServerVO>, Integer> result = searchForDnsServerInternal(cmd);
+ ListResponse<DnsServerResponse> response = new ListResponse<>();
+ if (result == null) {
+ return response;
+ }
+ List<String> serverIds = new ArrayList<>();
+ for (DnsServer server : result.first()) {
+ serverIds.add(server.getUuid());
+ }
+ List<DnsServerJoinVO> joinResult = dnsServerJoinDao.listByUuids(serverIds);
+ List<DnsServerResponse> serverResponses = new ArrayList<>();
+ for (DnsServerJoinVO server : joinResult) {
+ serverResponses.add(createDnsServerResponse(server));
+ }
+ response.setResponses(serverResponses, result.second());
+ return response;
+ }
+
+ private Pair<List<DnsServerVO>, Integer> searchForDnsServerInternal(ListDnsServersCmd cmd) {
+ Account caller = CallContext.current().getCallingAccount();
+ Set<Long> parentDomainIds = domainDao.getDomainParentIds(caller.getDomainId());
+ Filter searchFilter = new Filter(DnsServerVO.class, ApiConstants.ID, true, cmd.getStartIndex(), cmd.getPageSizeVal());
+ return dnsServerDao.searchDnsServer(cmd.getId(), 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);
+ if (dnsServer == null) {
+ throw new InvalidParameterValueException(String.format("DNS server with ID: %s not found.", dnsServerId));
+ }
+
+ Account caller = CallContext.current().getCallingAccount();
+ accountMgr.checkAccess(caller, null, true, dnsServer);
+
+ boolean validationRequired = false;
+ String originalUrl = dnsServer.getUrl();
+ String originalKey = dnsServer.getDnsApiKey();
+
+ 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.getDnsApiKey() != null && !cmd.getDnsApiKey().equals(originalKey)) {
+ dnsServer.setDnsApiKey(cmd.getDnsApiKey());
+ validationRequired = true;
+ }
+
+ if (cmd.getPort() != null && !Objects.equals(cmd.getPort(), dnsServer.getPort())) {
+ dnsServer.setPort(cmd.getPort());
+ validationRequired = true;
+ }
+ if (accountMgr.isRootAdmin(caller.getId()) || accountMgr.isDomainAdmin(caller.getId())) {
+ if (cmd.isPublic() != null) {
+ boolean isPublic = BooleanUtils.isTrue(cmd.isPublic());
+ dnsServer.setPublicServer(isPublic);
+
+ String publicDomainSuffix = null;
+ if (isPublic && StringUtils.isNotBlank(cmd.getPublicDomainSuffix())) {
+ publicDomainSuffix = DnsProviderUtil.normalizeDomainForDb(cmd.getPublicDomainSuffix());
+ }
+ dnsServer.setPublicDomainSuffix(publicDomainSuffix);
+ }
+ }
+
+ if (cmd.getNameServers() != null) {
+ dnsServer.setNameServers(cmd.getNameServers());
+ }
+ if (cmd.getState() != null) {
+ dnsServer.setState(cmd.getState());
+ }
+
+ if (validationRequired) {
+ DnsProvider provider = getProviderByType(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
+ @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);
+ if (dnsServer == null) {
+ throw new InvalidParameterValueException(String.format("DNS server with ID: %s not found.", dnsServerId));
+ }
+ Account caller = CallContext.current().getCallingAccount();
+ accountMgr.checkAccess(caller, null, true, dnsServer);
+ return Transaction.execute((TransactionCallback<Boolean>) status -> {
+ if (cmd.getCleanup()) {
+ List<Long> dnsZoneIds = dnsZoneDao.findDnsZoneIdsByServerId(dnsServerId);
+ for (Long dnsZoneId : dnsZoneIds) {
+ try {
+ deleteDnsZone(dnsZoneId, cmd.isUnmanage());
+ } catch (Exception ex) {
+ logger.error("Error cleaning up DNS zone: {} during DNS server: {} deletion", dnsZoneId, dnsServer.getName());
+ }
+ }
+ }
+ return dnsServerDao.remove(dnsServerId);
+ });
+ }
+
+ @Override
+ @ActionEvent(eventType = EventTypes.EVENT_DNS_ZONE_DELETE, eventDescription = "Deleting DNS Zone")
+ public boolean deleteDnsZone(Long zoneId, boolean retainInProvider) {
+ DnsZoneVO dnsZone = dnsZoneDao.findById(zoneId);
+ if (dnsZone == null) {
+ throw new InvalidParameterValueException("DNS zone not found for the given ID.");
+ }
+ String dnsZoneName = dnsZone.getName();
+ Account caller = CallContext.current().getCallingAccount();
+ accountMgr.checkAccess(caller, null, true, dnsZone);
+
+ boolean dbResult = Transaction.execute((TransactionCallback<Boolean>) status -> {
+ DnsZoneNetworkMapVO networkMapVO = dnsZoneNetworkMapDao.findByZoneId(zoneId);
+ // Remove DNS records from nic_details if there are any
+ if (networkMapVO != null) {
+ try {
+ List<NicDnsJoinVO> nicDnsJoinVOS = nicDnsJoinDao.listByZoneId(zoneId);
+ if (CollectionUtils.isNotEmpty(nicDnsJoinVOS)) {
+ Set<Long> nicIds = nicDnsJoinVOS.stream().map(NicDnsJoinVO::getId).collect(Collectors.toSet());
+ nicDetailsDao.removeDetailsForNicIds(ApiConstants.NIC_DNS_NAME, nicIds);
+ }
+ } catch (Exception ex) {
+ logger.warn("Failed to fetch DNS records for dnsZone: {}, perform manual cleanup.", dnsZoneName, ex);
+ }
+ dnsZoneNetworkMapDao.removeNetworkMappingByZoneId(zoneId);
+ }
+
+ // Remove DNS zone from provider and cleanup DB
+ if (!retainInProvider) {
+ try {
+ DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId());
+ if (server == null) {
+ throw new CloudRuntimeException(String.format("The DNS server not found for DNS zone: %s", dnsZoneName));
+ }
+ DnsProvider provider = getProviderByType(server.getProviderType());
+ provider.deleteZone(server, dnsZone);
+ logger.debug("Deleted DNS zone: {} from provider", dnsZoneName);
+ } catch (DnsNotFoundException ex) {
+ logger.warn("DNS zone: {} is not present in the provider, proceeding with cleanup", dnsZoneName);
+ } catch (Exception ex) {
+ logger.error("Failed to delete DNS zone from provider", ex);
+ if (ex instanceof CloudRuntimeException) {
+ throw new CloudRuntimeException(ex.getMessage());
+ }
+ throw new CloudRuntimeException(String.format("Failed to delete DNS zone: %s.", dnsZoneName));
+ }
+ }
+ return dnsZoneDao.remove(zoneId);
+ });
+
+ if (!dbResult) {
+ logger.error("Failed to remove DNS zone {} from DB after provider deletion", dnsZoneName);
+ }
+ return dbResult;
+ }
+
+ @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) {
+ throw new InvalidParameterValueException("DNS zone not found.");
+ }
+ Account caller = CallContext.current().getCallingAccount();
+ accountMgr.checkAccess(caller, null, true, dnsZone);
+ boolean updated = false;
+ if (cmd.getDescription() != null) {
+ dnsZone.setDescription(cmd.getDescription());
+ updated = true;
+ }
+
+ if (updated) {
+ 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 = getProviderByType(server.getProviderType());
+ provider.updateZone(server, dnsZone);
+ dnsZoneDao.update(dnsZone.getId(), 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 dnsZone;
+ }
+
+ @Override
+ public ListResponse<DnsZoneResponse> listDnsZones(ListDnsZonesCmd cmd) {
+ Pair<List<DnsZoneVO>, Integer> result = searchForDnsZonesInternal(cmd);
+ List<String> zoneIds = new ArrayList<>();
+ for (DnsZoneVO zone : result.first()) {
+ zoneIds.add(zone.getUuid());
+ }
+ List<DnsZoneJoinVO> zoneJoinVos = dnsZoneJoinDao.listByUuids(zoneIds);
+ List<DnsZoneResponse> zoneResponses = new ArrayList<>();
+ for (DnsZoneJoinVO zoneJoin: zoneJoinVos) {
+ zoneResponses.add(createDnsZoneResponse(zoneJoin));
+ }
+ ListResponse<DnsZoneResponse> response = new ListResponse<>();
+ response.setResponses(zoneResponses, result.second());
+ return response;
+ }
+
+ private Pair<List<DnsZoneVO>, 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());
+ if (dnsServer == null) {
+ throw new InvalidParameterValueException("DNS server not found for the provided ID");
+ }
+ accountMgr.checkAccess(caller, null, true, dnsServer);
+ }
+ List<Long> 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
+ @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)) {
+ throw new InvalidParameterValueException("Empty DNS record name is not allowed");
+ }
+ DnsZoneVO dnsZone = dnsZoneDao.findById(cmd.getDnsZoneId());
+ if (dnsZone == null) {
+ throw new InvalidParameterValueException("DNS zone not found.");
+ }
+ Account caller = CallContext.current().getCallingAccount();
+ accountMgr.checkAccess(caller, null, true, dnsZone);
+ DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId());
+ if (server == null) {
+ throw new CloudRuntimeException("The underlying DNS server for this DNS zone is missing.");
+ }
+ try {
+ DnsRecord.RecordType type = cmd.getType();
+ DnsProvider provider = getProviderByType(server.getProviderType());
+ if (provider.dnsRecordExists(server, dnsZone, recordName, type.name())) {
+ throw new CloudRuntimeException(String.format(
+ "DNS record '%s' of type '%s' already exists in DNS zone '%s' (provider: %s). Overwriting is not permitted.",
+ recordName, type.name(), dnsZone.getName(), provider.getName()
+ ));
+ }
+
+ List<String> normalizedContents = cmd.getContents().stream()
+ .map(value -> DnsProviderUtil.normalizeDnsRecordValue(value, type)).collect(Collectors.toList());
+ DnsRecord record = new DnsRecord(recordName, type, normalizedContents, cmd.getTtl());
+
+ String normalizedRecordName = provider.addRecord(server, dnsZone, 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", recordName));
+ }
+ }
+
+ @Override
+ @ActionEvent(eventType = EventTypes.EVENT_DNS_RECORD_DELETE, eventDescription = "Deleting DNS Record")
+ public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) {
+ DnsZoneVO dnsZone = dnsZoneDao.findById(cmd.getDnsZoneId());
+ if (dnsZone == null) {
+ throw new InvalidParameterValueException("DNS zone not found.");
+ }
+ Account caller = CallContext.current().getCallingAccount();
+ accountMgr.checkAccess(caller, null, true, dnsZone);
+ DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId());
+ if (server == null) {
+ throw new CloudRuntimeException("The underlying DNS server for this DNS Record is missing.");
+ }
+ DnsRecord.RecordType recordType = cmd.getType();
+ try {
+ DnsRecord record = new DnsRecord();
+ record.setName(cmd.getName());
+ record.setType(recordType);
+ DnsProvider provider = getProviderByType(server.getProviderType());
+ String deletedDnsRecord = provider.deleteRecord(server, dnsZone, record);
+ return deletedDnsRecord != 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()));
+ }
+ }
+
+ @Override
+ public ListResponse<DnsRecordResponse> listDnsRecords(ListDnsRecordsCmd cmd) {
+ DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId());
+ if (zone == null) {
+ throw new InvalidParameterValueException("DNS zone not found for the given ID.");
+ }
+ 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 DNS zone is missing.");
+ }
+ try {
+ DnsProvider provider = getProviderByType(server.getProviderType());
+ List<DnsRecord> records = provider.listRecords(server, zone);
+ List<DnsRecordResponse> responses = new ArrayList<>();
+ for (DnsRecord record : records) {
+ responses.add(createDnsRecordResponse(record));
+ }
+
+ ListResponse<DnsRecordResponse> 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");
+ }
+ }
+
+ @Override
+ public List<String> listProviderNames() {
+ List<String> providerNames = new ArrayList<>();
+ if (dnsProviders != null) {
+ for (DnsProvider provider : dnsProviders) {
+ providerNames.add(provider.getProviderType().toString());
+ }
+ }
+ return providerNames;
+ }
+
+ @Override
+ public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) {
+ if (StringUtils.isBlank(cmd.getName())) {
+ throw new InvalidParameterValueException("DNS zone name cannot be empty");
+ }
+
+ String dnsZoneName = DnsProviderUtil.normalizeDomainForDb(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.getPublicServer()) {
+ throw new PermissionDeniedException("You do not have permission to use this DNS server.");
+ }
+ dnsZoneName = DnsProviderUtil.appendPublicSuffixToZone(dnsZoneName, server.getPublicDomainSuffix());
+ }
+ 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(dnsZoneName, type, server.getId(), caller.getId(), caller.getDomainId(), cmd.getDescription());
+ return dnsZoneDao.persist(dnsZoneVO);
+ }
+
+ @Override
+ public DnsZone provisionDnsZone(long dnsZoneId, boolean isExistingZone) {
+ DnsZoneVO dnsZone = dnsZoneDao.findById(dnsZoneId);
+ if (dnsZone == null) {
+ throw new CloudRuntimeException("DNS zone not found during provisioning");
+ }
+ DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId());
+ if (server == null) {
+ dnsZoneDao.remove(dnsZoneId);
+ throw new CloudRuntimeException(String.format("DNS server for zone '%s' no longer exists", dnsZone.getName()));
+ }
+ try {
+ DnsProvider provider = getProviderByType(server.getProviderType());
+ if (isExistingZone) {
+ if (!provider.dnsZoneExists(server, dnsZone)) {
+ throw new CloudRuntimeException(
+ String.format("DNS zone '%s' cannot be imported because it does not exist in the DNS provider", dnsZone.getName()));
+ }
+ } else {
+ String externalReferenceId = provider.provisionZone(server, dnsZone);
+ dnsZone.setExternalReference(externalReferenceId);
+ }
+ dnsZone.setState(DnsZone.State.Active);
+ 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);
+ String errorMsg = "";
+ if (ex instanceof DnsConflictException) {
+ errorMsg = String.format("DNS zone: %s already exists", dnsZone.getName());
+ } else if (ex instanceof DnsTransportException) {
+ errorMsg = String.format("DNS server: %s not reachable", server.getName());
+ } else if (ex instanceof CloudRuntimeException) {
+ errorMsg = ex.getMessage();
+ }
+ throw new CloudRuntimeException(errorMsg);
+ }
+ return dnsZone;
+ }
+
+
+ public DnsServerResponse createDnsServerResponse(DnsServer dnsServer) {
+ DnsServerJoinVO serverJoin = dnsServerJoinDao.findById(dnsServer.getId());
+ if (serverJoin == null) {
+ throw new CloudRuntimeException(String.format("DNS server not found for ID: %s", 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.setState(server.getState().name());
+ response.setObjectName("dnsserver");
+ return response;
+ }
+
+ @Override
+ public DnsZoneResponse createDnsZoneResponse(DnsZone dnsZone) {
+ DnsZoneJoinVO zoneJoinVO = dnsZoneJoinDao.findById(dnsZone.getId());
+ if (zoneJoinVO == null) {
+ throw new CloudRuntimeException(String.format("DNS zone join view not found for ID: %s", 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
+ public DnsRecordResponse createDnsRecordResponse(DnsRecord record) {
+ DnsRecordResponse res = new DnsRecordResponse();
+ res.setName(record.getName());
+ res.setType(record.getType());
+ res.setContent(record.getContents());
+ res.setTtl(record.getTtl());
+ return res;
+ }
+
+ @Override
+ public DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetworkCmd cmd) {
+ Account caller = CallContext.current().getCallingAccount();
+ DnsZoneVO dnsZone = dnsZoneDao.findById(cmd.getDnsZoneId());
+ if (dnsZone == null) {
+ throw new InvalidParameterValueException("DNS zone not found.");
+ }
+ accountMgr.checkAccess(caller, null, true, dnsZone);
+ DnsServerVO dnsServer = dnsServerDao.findById(dnsZone.getDnsServerId());
+ if (dnsServer == null) {
+ throw new CloudRuntimeException("The underlying DNS server for this DNS zone is missing.");
+ }
+ 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.findByNetworkId(network.getId());
+ if (existing != null) {
+ throw new InvalidParameterValueException("Network has existing DNS zone associated to it.");
+ }
+
+ DnsZoneNetworkMapVO mapping = new DnsZoneNetworkMapVO(dnsZone.getId(), network.getId(), cmd.getSubDomain());
+ dnsZoneNetworkMapDao.persist(mapping);
+ DnsZoneNetworkMapResponse response = new DnsZoneNetworkMapResponse();
+ response.setId(mapping.getUuid());
+ response.setDnsZoneId(dnsZone.getUuid());
+ response.setNetworkId(network.getUuid());
+ response.setSubDomain(mapping.getSubDomain());
+ return response;
+ }
+
+ @Override
+ public boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd) {
+ DnsZoneNetworkMapVO mapping = dnsZoneNetworkMapDao.findByNetworkId(cmd.getNetworkId());
+ if (mapping == null) {
+ throw new InvalidParameterValueException("No DNS zone is associated to specified network.");
+ }
+ 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 void checkDnsServerPermission(Account caller, DnsServer dnsServer) throws PermissionDeniedException {
+ if (caller.getId() == dnsServer.getAccountId()) {
+ return;
+ }
+ if (!dnsServer.getPublicServer()) {
+ 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 checkDnsZonePermission(Account caller, DnsZone zone) {
+ if (caller.getId() != zone.getAccountId()) {
+ throw new PermissionDeniedException(caller + "is not allowed to access the DNS Zone " + zone.getName());
+ }
+ }
+
+ public Account getAccount(long accountId) {
+ return accountDao.findByIdIncludingRemoved(accountId);
+ }
+
+ @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 boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
+ messageBus.subscribe(VirtualMachineManager.Topics.VM_LIFECYCLE_STATE, new VmLifecycleSubscriber());
+ messageBus.subscribe(Nic.Topics.NIC_LIFECYCLE, new NicLifecycleSubscriber());
+ messageBus.subscribe(VirtualMachineManager.Topics.VM_ACTION, new VmRenameActionSubscriber());
+ return true;
+ }
+
+ @Override
+ public List<Class<?>> getCommands() {
+ List<Class<?>> cmdList = new ArrayList<>();
+
+ cmdList.add(ListDnsProvidersCmd.class);
+ // DNS Server Commands
+ cmdList.add(AddDnsServerCmd.class);
+ cmdList.add(ListDnsServersCmd.class);
+ cmdList.add(DeleteDnsServerCmd.class);
+ cmdList.add(UpdateDnsServerCmd.class);
+
+ // DNS Zone Commands
+ cmdList.add(CreateDnsZoneCmd.class);
+ cmdList.add(ListDnsZonesCmd.class);
+ cmdList.add(DeleteDnsZoneCmd.class);
+ cmdList.add(UpdateDnsZoneCmd.class);
+ cmdList.add(AssociateDnsZoneToNetworkCmd.class);
+ cmdList.add(DisassociateDnsZoneFromNetworkCmd.class);
+
+ // DNS Record Commands
+ cmdList.add(CreateDnsRecordCmd.class);
+ cmdList.add(ListDnsRecordsCmd.class);
+ cmdList.add(DeleteDnsRecordCmd.class);
+ return cmdList;
+ }
+
+ public List<DnsProvider> getDnsProviders() {
+ return dnsProviders;
+ }
+
+ public void setDnsProviders(List<DnsProvider> dnsProviders) {
+ this.dnsProviders = dnsProviders;
+ }
+
+ class VmLifecycleSubscriber implements MessageSubscriber {
+
+ @Override
+ public void onPublishMessage(String senderAddress, String subject, Object args) {
+ try {
+ logger.trace("VM lifecycle event: {}, {}, {}", senderAddress, subject, args);
+
+ @SuppressWarnings("unchecked")
+ Map<String, Object> event = (Map<String, Object>) args;
+ VirtualMachine.State oldState = (VirtualMachine.State) event.get(ApiConstants.OLD_STATE);
+ VirtualMachine.State newState = (VirtualMachine.State) event.get(ApiConstants.NEW_STATE);
+ if (oldState == newState) {
+ logger.debug("Instance state is unchanged, skip event processing");
+ return;
+ }
+ long instanceId = (long) event.get(ApiConstants.INSTANCE_ID);
+ switch (newState) {
+ case Running:
+ handleVmCreateEvent(instanceId);
+ break;
+ case Destroyed:
+ handleVmDestroyEvent(instanceId);
+ break;
+ default:
+ logger.warn("Ignoring lifecycle event for Instance ID: {}, unsupported state={}", instanceId, newState);
+ }
+ } catch (Exception ex) {
+ logger.error("Failed to process Instance lifecycle event", ex);
+ }
+ }
+ }
+
+ class NicLifecycleSubscriber implements MessageSubscriber {
+ @Override
+ public void onPublishMessage(String senderAddress, String subject, Object args) {
+ try {
+ logger.trace("NIC lifecycle event: {}, {}, {}", senderAddress, subject, args);
+
+ @SuppressWarnings("unchecked")
+ Map<String, Object> event = (Map<String, Object>) args;
+ String eventType = (String) event.get(ApiConstants.EVENT_TYPE);
+ Long nicId = (Long) event.get(ApiConstants.NIC_ID);
+ Long instanceId = (Long) event.get(ApiConstants.INSTANCE_ID);
+ if (nicId == null || instanceId == null) {
+ logger.warn("Insufficient data to process NIC lifecycle event: {}", args);
+ return;
+ }
+
+ switch (eventType) {
+ case EVENT_NIC_CREATE:
+ handleNicPlug(instanceId, nicId);
+ break;
+ case EVENT_NIC_DELETE:
+ handleNicUnplug(instanceId, nicId);
+ break;
+ case EVENT_NIC_UPDATE:
+ handleNicRefresh(instanceId, nicId);
+ break;
+ default:
+ logger.warn("Ignoring lifecycle event for NIC with ID: {}, unsupported eventType={}", nicId, eventType);
+ }
+ } catch (Exception ex) {
+ logger.error("Failed to process NIC lifecycle event", ex);
+ }
+
+ }
+ }
+
+ class VmRenameActionSubscriber implements MessageSubscriber {
+
+ @Override
+ public void onPublishMessage(String senderAddress, String subject, Object args) {
+ try {
+ logger.trace("VM action event: {}, {}, {}", senderAddress, subject, args);
+
+ @SuppressWarnings("unchecked")
+ Map<String, Object> event = (Map<String, Object>) args;
+ String eventType = (String) event.get(ApiConstants.EVENT_TYPE);
+ if (!eventType.equalsIgnoreCase(EVENT_VM_UPDATE)) {
+ return;
+ }
+ String newHostName = (String) event.get(ApiConstants.HOST_NAME);
+ String oldHostName = (String) event.get(ApiConstants.OLD_HOST_NAME);
+ if (oldHostName.equalsIgnoreCase(newHostName)) {
+ logger.debug("Instance hostname is unchanged, skip event processing");
+ return;
+ }
+ long instanceId = (long) event.get(ApiConstants.INSTANCE_ID);
+ handleVmHostnameChanged(instanceId, newHostName);
+ } catch (Exception ex) {
+ logger.error("Failed to process Instance action event", ex);
+ }
+ }
+ }
+
+ void handleVmCreateEvent(long instanceId) {
+ VirtualMachine instance = vmInstanceDao.findById(instanceId);
+ if (instance == null) {
+ logger.debug("Instance is not found for the given ID: {}", instanceId);
+ return;
+ }
+ List<NicDnsJoinVO> mappedNics = nicDnsJoinDao.listActiveByVmId(instanceId);
+ if (CollectionUtils.isEmpty(mappedNics)) {
+ logger.debug("No active DNS zone associated to NICs");
+ return;
+ }
+ Map<Long, Map<String, List<NicDnsJoinVO>>> dnsZoneRecordNicMap = new HashMap<>();
+ for (NicDnsJoinVO nic : mappedNics) {
+ DnsZoneVO targetZone = dnsZoneDao.findById(nic.getDnsZoneId());
+ if (targetZone == null) {
+ continue;
+ }
+ String targetDnsRecordUrl = prepareDnsRecordUrl(instance.getHostName(), nic.getSubDomain(), targetZone.getName());
+ dnsZoneRecordNicMap.computeIfAbsent(targetZone.getId(), k -> new HashMap<>())
+ .computeIfAbsent(targetDnsRecordUrl, k -> new ArrayList<>())
+ .add(nic);
+ }
+
+ for (Map.Entry<Long, Map<String, List<NicDnsJoinVO>>> zoneEntry : dnsZoneRecordNicMap.entrySet()) {
+ long targetZoneId = zoneEntry.getKey();
+ for (Map.Entry<String, List<NicDnsJoinVO>> dnsUrlEntry : zoneEntry.getValue().entrySet()) {
+ String dnsRecordUrl = dnsUrlEntry.getKey();
+ List<NicDnsJoinVO> nicsForThisFqdn = dnsUrlEntry.getValue();
+ try {
+ Transaction.execute(new TransactionCallbackWithExceptionNoReturn<DnsProviderException>() {
+ @Override
+ public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException{
+ if (isDnsCollision(dnsRecordUrl, targetZoneId, instanceId)) {
+ return;
+ }
+ for (NicDnsJoinVO nic : nicsForThisFqdn) {
+ nicDetailsDao.addDetail(nic.getId(), ApiConstants.NIC_DNS_NAME, dnsRecordUrl, true);
+ }
+ syncDnsRecordsState(instanceId, dnsRecordUrl, targetZoneId);
+ }
+ });
+ } catch (DnsProviderException ex) {
+ logger.error("Failed to register DNS record: {} in provider for Instance: {}", dnsRecordUrl, instance.getUuid(), ex);
+ } catch (Exception ex) {
+ logger.warn("Failed to register DNS record: {} for Instance: {}", dnsRecordUrl, instance.getUuid(), ex);
+ }
+ }
+ }
+ }
+
+ void handleVmDestroyEvent(long instanceId) {
+ List<NicDnsJoinVO> historicalNics = nicDnsJoinDao.listIncludingRemovedByVmId(instanceId);
+ if (CollectionUtils.isEmpty(historicalNics)) {
+ return;
+ }
+ Map<Long, Map<String, List<NicDnsJoinVO>>> groupByDnsZone = new HashMap<>();
+ for (NicDnsJoinVO nic : historicalNics) {
+ // If the DNS record url is null, it means this NIC was never registered in nic_details
+ if (nic.getNicDnsName() == null) {
+ continue;
+ }
+ groupByDnsZone
+ .computeIfAbsent(nic.getDnsZoneId(), k -> new HashMap<>())
+ .computeIfAbsent(nic.getNicDnsName(), k -> new ArrayList<>())
+ .add(nic);
+ }
+
+ for (Map.Entry<Long, Map<String, List<NicDnsJoinVO>>> zoneEntry : groupByDnsZone.entrySet()) {
+ long targetZoneId = zoneEntry.getKey();
+ for (Map.Entry<String, List<NicDnsJoinVO>> dnsRecordEntry : zoneEntry.getValue().entrySet()) {
+ String dnsRecordUrl = dnsRecordEntry.getKey();
+ List<NicDnsJoinVO> nicsForDnsUrl = dnsRecordEntry.getValue();
+
+ try {
+ Transaction.execute(new TransactionCallbackWithExceptionNoReturn<DnsProviderException>() {
+ @Override
+ public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException {
+ for (NicDnsJoinVO nic : nicsForDnsUrl) {
+ nicDetailsDao.removeDetail(nic.getId(), ApiConstants.NIC_DNS_NAME);
+ }
+ // Because we just deleted the nic_details, the sync method will naturally
+ // find 0 active IPs for this VM/FQDN combo and issue a clean DELETE to PowerDNS.
+ syncDnsRecordsState(instanceId, dnsRecordUrl, targetZoneId);
+ }
+ });
+ logger.debug("Successfully cleaned up DNS record: {} for Instance with ID: {}", dnsRecordUrl, instanceId);
+ } catch (DnsProviderException ex) {
+ logger.error("Failed to cleanup DNS record: {} in provider", dnsRecordUrl, ex);
+ } catch (Exception ex) {
+ logger.error("Failed DNS record: {} cleanup for Instance", dnsRecordUrl, ex);
+ }
+ }
+ }
+ }
+
+ void handleVmHostnameChanged(long instanceId, String newHostName) {
+ VirtualMachine instance = vmInstanceDao.findById(instanceId);
+ if (instance == null) {
+ logger.debug("Instance is not found for the given ID: {}", instanceId);
+ return;
+ }
+
+ List<NicDnsJoinVO> mappedNics = nicDnsJoinDao.listActiveByVmId(instanceId);
+ if (CollectionUtils.isEmpty(mappedNics)) {
+ return;
+ }
+
+ Map<Long, Map<String, List<NicDnsJoinVO>>> dnsZoneNewRecordNicMap = new HashMap<>();
+ for (NicDnsJoinVO nic : mappedNics) {
+ DnsZoneVO targetZone = dnsZoneDao.findById(nic.getDnsZoneId());
+ if (targetZone == null) {
+ continue;
+ }
+
+ String oldDnsRecordUrl = nic.getNicDnsName();
+ String newDnsRecordUrl = prepareDnsRecordUrl(newHostName, nic.getSubDomain(), targetZone.getName());
+ if (newDnsRecordUrl.equals(oldDnsRecordUrl)) {
+ continue;
+ }
+
+ dnsZoneNewRecordNicMap.computeIfAbsent(targetZone.getId(), k -> new HashMap<>())
+ .computeIfAbsent(newDnsRecordUrl, k -> new ArrayList<>())
+ .add(nic);
+ }
+
+ for (Map.Entry<Long, Map<String, List<NicDnsJoinVO>>> zoneEntry : dnsZoneNewRecordNicMap.entrySet()) {
+ long targetZoneId = zoneEntry.getKey();
+
+ for (Map.Entry<String, List<NicDnsJoinVO>> newUrlEntry : zoneEntry.getValue().entrySet()) {
+ String newDnsRecordUrl = newUrlEntry.getKey();
+ List<NicDnsJoinVO> nicsForThisFqdn = newUrlEntry.getValue();
+
+ try {
+ Set<String> oldDnsRecordUrls = new HashSet<>();
+ Transaction.execute(new TransactionCallbackWithExceptionNoReturn<DnsProviderException>() {
+ @Override
+ public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException {
+ for (NicDnsJoinVO nic : nicsForThisFqdn) {
+ if (nic.getNicDnsName() != null) {
+ oldDnsRecordUrls.add(nic.getNicDnsName());
+ nicDetailsDao.removeDetail(nic.getId(), ApiConstants.NIC_DNS_NAME);
+ }
+ }
+ for (String oldUrl : oldDnsRecordUrls) {
+ syncDnsRecordsState(instanceId, oldUrl, targetZoneId);
+ }
+ }
+ });
+
+ Transaction.execute(new TransactionCallbackWithExceptionNoReturn<DnsProviderException>() {
+ @Override
+ public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException {
+ if (isDnsCollision(newDnsRecordUrl, targetZoneId, instanceId)) {
+ return;
+ }
+ for (NicDnsJoinVO nic : nicsForThisFqdn) {
+ nicDetailsDao.addDetail(nic.getId(), ApiConstants.NIC_DNS_NAME, newDnsRecordUrl, true);
+ }
+ // This sync call finds the newly written intent and sends an ADD/REPLACE call.
+ syncDnsRecordsState(instanceId, newDnsRecordUrl, targetZoneId);
+ }
+ });
+ logger.debug("Successfully handled DNS Rename to: {}", newDnsRecordUrl);
+
+ } catch (Exception ex) {
+ logger.error("Failed to process VM Rename for Instance: {}", instance.getUuid(), ex);
+ throw new CloudRuntimeException(String.format("DNS API Sync Failed for Rename to %s", newDnsRecordUrl), ex);
+ }
+ }
+ }
+ }
+
+ void handleNicPlug(long instanceId, long nicId) {
+ VirtualMachine instance = vmInstanceDao.findById(instanceId);
+ if (instance == null || !VM_ALLOWED_STATES.contains(instance.getState())) {
+ return;
+ }
+ NicDnsJoinVO nic = nicDnsJoinDao.findById(nicId);
+ if (nic == null) {
+ logger.debug("NIC with ID: {} doesn't have DNS zone associated", nicId);
+ return;
+ }
+
+ DnsZoneVO targetZone = dnsZoneDao.findById(nic.getDnsZoneId());
+ if (targetZone == null) {
+ return;
+ }
+
+ String dnsRecordUrl = prepareDnsRecordUrl(instance.getHostName(), nic.getSubDomain(), targetZone.getName());
+ try {
+ Transaction.execute(new TransactionCallbackWithExceptionNoReturn<DnsProviderException>() {
+
+ @Override
+ public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException {
+ if (isDnsCollision(dnsRecordUrl, targetZone.getId(), instanceId)) {
+ return;
+ }
+ nicDetailsDao.addDetail(nicId, ApiConstants.NIC_DNS_NAME, dnsRecordUrl, true);
+ syncDnsRecordsState(instanceId, dnsRecordUrl, targetZone.getId());
+ logger.debug("Successfully synced DNS on NIC Plug for: {}", dnsRecordUrl);
+ }
+ });
+ } catch (DnsProviderException ex) {
+ logger.error("Failed to register DNS record: {} in provider for Instance: {}", dnsRecordUrl, instance.getUuid(), ex);
+ } catch (Exception ex) {
+ logger.warn("Failed to register DNS record: {} for Instance: {}", dnsRecordUrl, instance.getUuid(), ex);
+ }
+ }
+
+ void handleNicUnplug(long instanceId, long nicId) {
+ NicDnsJoinVO nic = nicDnsJoinDao.findByIdIncludingRemoved(nicId);
+ if (nic == null || nic.getNicDnsName() == null) {
+ return;
+ }
+ String dnsRecordUrl = nic.getNicDnsName();
+ long dnsZoneId = nic.getDnsZoneId();
+
+ try {
+ Transaction.execute(new TransactionCallbackWithExceptionNoReturn<DnsProviderException>() {
+ @Override
+ public void doInTransactionWithoutResult(TransactionStatus status) throws DnsProviderException {
+ nicDetailsDao.removeDetail(nicId, ApiConstants.NIC_DNS_NAME);
+ syncDnsRecordsState(instanceId, dnsRecordUrl, dnsZoneId);
+ logger.debug("Successfully synced DNS record: {} on NIC unplug", dnsRecordUrl);
+ }
+ });
+ } catch (DnsProviderException ex) {
+ logger.error("Failed to sync DNS record: {} on NIC unplug in provider", dnsRecordUrl);
+ } catch (Exception ex) {
+ logger.error("Failed to sync DNS record: {} on NIC unplug ", dnsRecordUrl);
+ }
+ }
+
+ private void handleNicRefresh(long instanceId, long nicId) {
+ handleNicUnplug(instanceId, nicId);
+ handleNicPlug(instanceId, nicId);
+ }
+
+ String prepareDnsRecordUrl(String hostName, String subDomain, String dnsZoneName) {
+ List<String> parts = new ArrayList<>();
+ parts.add(hostName);
+ if (StringUtils.isNotBlank(subDomain)) {
+ parts.add(subDomain.trim());
+ }
+ parts.add(dnsZoneName);
+ return String.join(".", parts);
+ }
+
+ private boolean isDnsCollision(String dnsRecordUrl, long targetZoneId, long instanceId) {
+ NicDnsJoinVO existing = nicDnsJoinDao.findActiveByDnsRecordAndZone(targetZoneId, dnsRecordUrl);
+ if (existing != null && existing.getInstanceId() != instanceId) {
+ logger.error("DNS collision: cannot register DNS record: {}. Already owned by Instance: {}.",
+ dnsRecordUrl, existing.getInstanceId());
+
+ String description = String.format("Instance hostname change resulted in a DNS collision. " +
+ "The requested DNS record '%s' is already in use.", dnsRecordUrl);
+ ActionEventUtils.onActionEvent(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, Domain.ROOT_DOMAIN,
+ EventTypes.EVENT_DNS_NAME_COLLISION, description, instanceId, ApiCommandResourceType.VirtualMachine.toString());
+ return true;
+ }
+ return false;
+ }
+
+ public void syncDnsRecordsState(Long instanceId, String dnsRecordUrl, long dnsZoneId) throws DnsProviderException {
+ DnsZone dnsZone = dnsZoneDao.findById(dnsZoneId);
+ if (dnsZone == null) {
+ logger.error("DNS zone not found for the provided ID: {}", dnsZoneId);
+ return;
+ }
+ DnsServerVO dnsServer = dnsServerDao.findById(dnsZone.getDnsServerId());
+ List<NicDnsJoinVO> activeNics = nicDnsJoinDao.listActiveByVmIdZoneAndDnsRecord(instanceId, dnsZoneId, dnsRecordUrl);
+
+ List<String> ipv4s = new ArrayList<>();
+ List<String> ipv6s = new ArrayList<>();
+ for (NicDnsJoinVO nic : activeNics) {
+ if (nic.getIp4Address() != null && !nic.getIp4Address().isEmpty()) {
+ ipv4s.add(nic.getIp4Address());
+ }
+ if (nic.getIp6Address() != null && !nic.getIp6Address().isEmpty()) {
+ ipv6s.add(nic.getIp6Address());
+ }
+ }
+ logger.debug("Syncing DNS for: {}. Found: {} IPv4s and {} IPv6s.", dnsRecordUrl, ipv4s.size(), ipv6s.size());
+ DnsProvider provider = getProviderByType(dnsServer.getProviderType());
+ DnsRecord recordIpv4 = new DnsRecord(dnsRecordUrl, DnsRecord.RecordType.A, null, 3600);
+ // push A records into Provider
+ if (ipv4s.isEmpty()) {
+ provider.deleteRecord(dnsServer, dnsZone, recordIpv4);
+ } else {
+ recordIpv4.setContents(ipv4s);
+ provider.addRecord(dnsServer, dnsZone, recordIpv4);
+ }
+ // push AAAA records into Provider
+ DnsRecord recordIpv6 = new DnsRecord(dnsRecordUrl, DnsRecord.RecordType.AAAA, null, 3600);
+ if (ipv6s.isEmpty()) {
+ provider.deleteRecord(dnsServer, dnsZone, recordIpv6);
+ } else {
+ recordIpv6.setContents(ipv6s);
+ provider.addRecord(dnsServer, dnsZone, recordIpv6);
+ }
+ }
+}
diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderUtil.java b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderUtil.java
new file mode 100644
index 00000000000..a3829af3151
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderUtil.java
@@ -0,0 +1,249 @@
+// 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.net.Inet4Address;
+import java.net.Inet6Address;
+
+import org.apache.commons.validator.routines.DomainValidator;
+
+import com.cloud.utils.StringUtils;
+import com.google.common.net.InetAddresses;
+
+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.normalizeDomainForDb(suffixDomain);
+ // Already suffixed → return as-is
+ if (zoneName.toLowerCase().endsWith("." + suffixDomain)) {
+ 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];
+ }
+
+ // lowercase, no trailing dot (used for DB storage, comparisons)
+ public static String normalizeDomainForDb(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
+ boolean valid = isValidInternalDnsZoneName(normalized) || validator.isValid(normalized);
+ if (!valid) {
+ throw new IllegalArgumentException("Invalid domain name: " + domain);
+ }
+ return normalized;
+ }
+
+ static boolean isValidInternalDnsZoneName(String domain) {
+ // Total length limit (DNS standard)
+ if (domain.length() > 253) {
+ return false;
+ }
+
+ String[] labels = domain.split("\\.");
+ // Must have at least 2 labels (zone + suffix like internal/test/etc.)
+ if (labels.length < 2) {
+ return false;
+ }
+ for (String label : labels) {
+ if (label.isEmpty() || label.length() > 63) {
+ return false;
+ }
+
+ // Must start and end with alphanumeric (even if underscores exist inside)
+ if (!label.matches("^[a-z0-9](?:[a-z0-9_-]{0,61}[a-z0-9])?$")) {
+ return false;
+ }
+
+ // Prevent obviously invalid cases like "__" only labels
+ if (label.equals("_")) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // DNS wire form: lowercase, validated, WITH trailing dot (used in record values)
+ public static String normalizeDnsRecordValue(String value, DnsRecord.RecordType recordType) {
+ if (StringUtils.isBlank(value)) {
+ throw new IllegalArgumentException("DNS record value cannot be empty");
+ }
+ String trimmedValue = value.trim();
+ switch (recordType) {
+ case A:
+ if (!(InetAddresses.forString(trimmedValue) instanceof Inet4Address)) {
+ throw new IllegalArgumentException(
+ String.format("Invalid IP address for %s record: %s", recordType, value));
+ }
+ return trimmedValue;
+ case AAAA:
+ if (!(InetAddresses.forString(trimmedValue) instanceof Inet6Address)) {
+ throw new IllegalArgumentException(
+ String.format("Invalid IP address for %s record: %s", recordType, value));
+ }
+ return trimmedValue;
+ case CNAME:
+ case NS:
+ case PTR:
+ return normalizeDomainForDnsRecord(trimmedValue);
+ case SRV:
+ return normalizeSrvRecord(trimmedValue);
+ case MX:
+ return normalizeMxRecord(trimmedValue);
+ case TXT:
+ // Free text: preserve exactly
+ return trimmedValue;
+ default:
+ throw new IllegalArgumentException("Unsupported DNS record type: " + recordType);
+ }
+ }
+
+ static String normalizeDomainForDnsRecord(String domain) {
+ if (StringUtils.isBlank(domain)) {
+ throw new IllegalArgumentException("Domain name cannot be empty");
+ }
+ String normalized = domain.trim().toLowerCase();
+ // Strip trailing dot first (normalize input)
+ if (normalized.endsWith(".")) {
+ normalized = normalized.substring(0, normalized.length() - 1);
+ }
+
+ // Reject IP addresses
+ if (InetAddresses.isInetAddress(normalized)) {
+ throw new IllegalArgumentException("Domain cannot be an IP address: " + normalized);
+ }
+
+ // Validate total length (max 253 chars, excluding trailing dot)
+ if (normalized.length() > 253) {
+ throw new IllegalArgumentException(
+ "Domain name exceeds maximum length: " + normalized);
+ }
+
+ // Validate labels
+ String[] labels = normalized.split("\\.", -1);
+ for (String label : labels) {
+ if (label.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Domain contains empty label: " + normalized);
+ }
+ if (label.length() > 63) {
+ throw new IllegalArgumentException(
+ "Domain label too long: " + label);
+ }
+ if (!label.matches("[a-z0-9]([a-z0-9-]*[a-z0-9])?")) {
+ throw new IllegalArgumentException(
+ "Invalid domain label: " + label);
+ }
+ }
+ return normalized + ".";
+ }
+
+ private static String normalizeSrvRecord(String value) {
+ String trimmed = value.trim();
+ String[] parts = trimmed.split("\\s+", 4);
+ if (parts.length != 4) {
+ throw new IllegalArgumentException(
+ "Invalid SRV record value (expected '<priority> <weight> <port> <target>'): " + trimmed);
+ }
+
+ int priority;
+ int weight;
+ int port;
+
+ try {
+ priority = Integer.parseInt(parts[0]);
+ weight = Integer.parseInt(parts[1]);
+ port = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "SRV priority/weight/port must be numeric: " + trimmed);
+ }
+
+ if (priority < 0 || priority > 65535) {
+ throw new IllegalArgumentException("SRV priority out of range (0–65535): " + parts[0]);
+ }
+
+ if (weight < 0 || weight > 65535) {
+ throw new IllegalArgumentException("SRV weight out of range (0–65535): " + parts[1]);
+ }
+
+ if (port < 1 || port > 65535) {
+ throw new IllegalArgumentException("SRV port out of range (1–65535): " + parts[2]);
+ }
+
+ String target = normalizeDomainForDnsRecord(parts[3]);
+
+ return priority + " " + weight + " " + port + " " + target;
+ }
+
+ private static String normalizeMxRecord(String value) {
+ String trimmed = value.trim();
+ String[] parts = trimmed.split("\\s+", 2);
+
+ if (parts.length != 2) {
+ throw new IllegalArgumentException(
+ "Invalid MX record value (expected '<priority> <mail-exchanger>'): " + trimmed);
+ }
+
+ int priority;
+
+ try {
+ priority = Integer.parseInt(parts[0]);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "MX priority must be numeric: " + parts[0]);
+ }
+
+ if (priority < 0 || priority > 65535) {
+ throw new IllegalArgumentException(
+ "MX priority out of range (0–65535): " + parts[0]);
+ }
+
+ String mailExchanger = normalizeDomainForDnsRecord(parts[1]);
+
+ return priority + " " + mailExchanger;
+ }
+}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java
similarity index 51%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java
index 2ca901fa3e5..cf8c91ae9d3 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java
@@ -14,12 +14,31 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+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;
+
+import com.cloud.utils.Pair;
+import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+public interface DnsServerDao extends GenericDao<DnsServerVO, Long> {
+
+ DnsServerVO findById(Long dnsServerId);
+
+ DnsServer findByUrlAndAccount(String url, long accountId);
+
+ List<Long> listDnsServerIdsByAccountId(Long accountId);
+
+ Pair<List<DnsServerVO>, Integer> searchDnsServer(Long dnsServerId, Long accountId, Set<Long> domainIds, DnsProviderType providerType, String keyword, Filter filter);
+
+ void loadDetails(DnsServer dnsServer);
+
+ void saveDetails(DnsServer dnsServer);
}
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 00000000000..e21602356ee
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java
@@ -0,0 +1,196 @@
+// 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.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.dns.DnsProviderType;
+import org.apache.cloudstack.dns.DnsServer;
+import org.apache.cloudstack.dns.vo.DnsServerDetailVO;
+import org.apache.cloudstack.dns.vo.DnsServerVO;
+import org.apache.commons.collections.CollectionUtils;
+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.GenericSearchBuilder;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import com.cloud.utils.db.Transaction;
+import com.cloud.utils.db.TransactionCallback;
+
+@Component
+public class DnsServerDaoImpl extends GenericDaoBase<DnsServerVO, Long> implements DnsServerDao {
+ @Inject
+ DnsServerDetailsDao dnsServerDetailsDao;
+
+ SearchBuilder<DnsServerVO> AllFieldsSearch;
+ SearchBuilder<DnsServerVO> AccountUrlSearch;
+ GenericSearchBuilder<DnsServerVO, Long> DnsServerIdsByAccountSearch;
+
+
+ public DnsServerDaoImpl() {
+ super();
+
+ 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();
+
+ 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();
+
+ }
+
+ @Override
+ public DnsServerVO findById(Long dnsServerId) {
+ DnsServerVO dnsServer = super.findById(dnsServerId);
+ loadDetails(dnsServer);
+ return dnsServer;
+ }
+
+ @Override
+ public DnsServer findByUrlAndAccount(String url, long accountId) {
+ SearchCriteria<DnsServerVO> sc = AccountUrlSearch.create();
+ sc.setParameters(ApiConstants.URL, url);
+ sc.setParameters(ApiConstants.ACCOUNT_ID, accountId);
+ return findOneBy(sc);
+ }
+
+ @Override
+ public List<Long> listDnsServerIdsByAccountId(Long accountId) {
+ SearchCriteria<Long> sc = DnsServerIdsByAccountSearch.create();
+ if (accountId != null) {
+ sc.setParameters(ApiConstants.ACCOUNT_ID, accountId);
+ }
+ sc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled);
+ return customSearch(sc, null);
+ }
+
+ @Override
+ public Pair<List<DnsServerVO>, Integer> searchDnsServer(Long dnsServerId, Long accountId, Set<Long> domainIds, DnsProviderType providerType,
+ String keyword, Filter filter) {
+
+ SearchBuilder<DnsServerVO> 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().getPublicServer(), 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<DnsServerVO> 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);
+ }
+
+ @Override
+ public DnsServerVO persist(DnsServerVO dnsServer) {
+ return Transaction.execute((TransactionCallback<DnsServerVO>) status -> {
+ DnsServerVO dnsServerDb = super.persist(dnsServer);
+ saveDetails(dnsServer);
+ loadDetails(dnsServerDb);
+ return dnsServerDb;
+ });
+ }
+
+ @Override
+ public boolean update(Long id, DnsServerVO dnsServer) {
+ return Transaction.execute((TransactionCallback<Boolean>) status -> {
+ boolean result = super.update(id, dnsServer);
+ if (result) {
+ saveDetails(dnsServer);
+ }
+ return result;
+ });
+ }
+
+ @Override
+ public boolean remove(Long dnsServerId) {
+ return Transaction.execute((TransactionCallback<Boolean>) status -> {
+ boolean result = super.remove(dnsServerId);
+ if (result) {
+ dnsServerDetailsDao.removeDetails(dnsServerId);
+ }
+ return result;
+ });
+ }
+
+ @Override
+ public void loadDetails(DnsServer dnsServer) {
+ if (dnsServer == null) {
+ return;
+ }
+ Map<String, String> details = dnsServerDetailsDao.listDetailsKeyPairs(dnsServer.getId());
+ dnsServer.setDetails(details);
+ }
+
+ @Override
+ public void saveDetails(DnsServer dnsServer) {
+ Map<String, String> detailsStr = dnsServer.getDetails();
+ if (detailsStr == null) {
+ return;
+ }
+ List<DnsServerDetailVO> details = new ArrayList<>();
+ for (String key : detailsStr.keySet()) {
+ DnsServerDetailVO detail = new DnsServerDetailVO(dnsServer.getId(), key, detailsStr.get(key), true);
+ details.add(detail);
+ }
+ dnsServerDetailsDao.saveDetails(details);
+ }
+}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDetailsDao.java
similarity index 80%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDetailsDao.java
index 2ca901fa3e5..ae900fbcddf 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDetailsDao.java
@@ -14,12 +14,13 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
+package org.apache.cloudstack.dns.dao;
+
+import org.apache.cloudstack.dns.vo.DnsServerDetailVO;
import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+public interface DnsServerDetailsDao extends GenericDao<DnsServerDetailVO, Long>, ResourceDetailsDao<DnsServerDetailVO> {
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDetailsDaoImpl.java
similarity index 76%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java
copy to server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDetailsDaoImpl.java
index 2a0b02aae1d..7f62921290f 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDaoImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDetailsDaoImpl.java
@@ -14,20 +14,16 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
+package org.apache.cloudstack.dns.dao;
-import org.springframework.stereotype.Component;
-
+import org.apache.cloudstack.dns.vo.DnsServerDetailVO;
import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase;
-import com.cloud.vm.NicDetailVO;
-
-@Component
-public class NicDetailsDaoImpl extends ResourceDetailsDaoBase<NicDetailVO> implements NicDetailsDao {
+public class DnsServerDetailsDaoImpl extends ResourceDetailsDaoBase<DnsServerDetailVO> implements DnsServerDetailsDao {
@Override
public void addDetail(long resourceId, String key, String value, boolean display) {
- super.addDetail(new NicDetailVO(resourceId, key, value, display));
+ super.addDetail(new DnsServerDetailVO(resourceId, key, value, display));
}
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDao.java
similarity index 78%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDao.java
index 2ca901fa3e5..b1932d6709c 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDao.java
@@ -14,12 +14,13 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.dao;
+
+import org.apache.cloudstack.dns.vo.DnsServerJoinVO;
import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+public interface DnsServerJoinDao extends GenericDao<DnsServerJoinVO, Long> {
+
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDaoImpl.java
similarity index 75%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDaoImpl.java
index 2ca901fa3e5..28a2e9d6638 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDaoImpl.java
@@ -14,12 +14,12 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.dao;
-import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
+import org.apache.cloudstack.dns.vo.DnsServerJoinVO;
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+import com.cloud.utils.db.GenericDaoBase;
+
+public class DnsServerJoinDaoImpl extends GenericDaoBase<DnsServerJoinVO, Long> implements DnsServerJoinDao {
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java
similarity index 55%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java
index 2ca901fa3e5..43bf60818d5 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java
@@ -14,12 +14,24 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.dao;
+import java.util.List;
+
+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;
-import com.cloud.vm.NicDetailVO;
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+public interface DnsZoneDao extends GenericDao<DnsZoneVO, Long> {
+ List<DnsZoneVO> listByAccount(long accountId);
+ DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type);
+
+ Pair<List<DnsZoneVO>, Integer> searchZones(Long id, Long accountId, List<Long> ownDnsServerIds, Long targetDnsServerId,
+ String keyword, Filter filter);
+
+ List<Long> findDnsZoneIdsByServerId(long dnsServerId);
}
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 00000000000..2487f2fee20
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java
@@ -0,0 +1,128 @@
+// 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.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+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;
+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 DnsZoneDaoImpl extends GenericDaoBase<DnsZoneVO, Long> implements DnsZoneDao {
+ SearchBuilder<DnsZoneVO> DnsServerSearch;
+ SearchBuilder<DnsZoneVO> AccountSearch;
+ SearchBuilder<DnsZoneVO> NameServerTypeSearch;
+
+ public DnsZoneDaoImpl() {
+ super();
+
+ DnsServerSearch = createSearchBuilder();
+ DnsServerSearch.selectFields(DnsServerSearch.entity().getId());
+ DnsServerSearch.and(ApiConstants.DNS_SERVER_ID, DnsServerSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ);
+ DnsServerSearch.done();
+
+ 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(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();
+ }
+
+ @Override
+ public List<DnsZoneVO> listByAccount(long accountId) {
+ SearchCriteria<DnsZoneVO> sc = AccountSearch.create();
+ sc.setParameters(ApiConstants.ACCOUNT_ID, accountId);
+ sc.setParameters(ApiConstants.STATE, DnsZone.State.Active);
+ return listBy(sc);
+ }
+
+ @Override
+ public DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type) {
+ SearchCriteria<DnsZoneVO> sc = NameServerTypeSearch.create();
+ sc.setParameters(ApiConstants.NAME, name);
+ 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<List<DnsZoneVO>, Integer> searchZones(Long id, Long accountId, List<Long> ownDnsServerIds, Long targetDnsServerId,
+ String keyword, Filter filter) {
+
+ SearchBuilder<DnsZoneVO> 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<DnsZoneVO> sc = sb.create();
+ if (id != null) {
+ sc.setParameters(ApiConstants.ID, id);
+ }
+ if (!CollectionUtils.isEmpty(ownDnsServerIds)) {
+ sc.setParameters(ApiConstants.DNS_SERVER_ID, ownDnsServerIds.toArray());
+ }
+ if (keyword != null) {
+ sc.setParameters(ApiConstants.NAME, "%" + keyword + "%");
+ }
+ 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);
+ }
+
+ public List<Long> findDnsZoneIdsByServerId(long dnsServerId) {
+ SearchCriteria<DnsZoneVO> sc = DnsServerSearch.create();
+ sc.setParameters(ApiConstants.DNS_SERVER_ID, dnsServerId);
+ List<DnsZoneVO> dnsZones = listBy(sc);
+ if (CollectionUtils.isEmpty(dnsZones)) {
+ return new ArrayList<>();
+ }
+ return dnsZones.stream().map(DnsZoneVO::getId).collect(Collectors.toList());
+ }
+}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDao.java
similarity index 78%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDao.java
index 2ca901fa3e5..ee0bcc5bcae 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDao.java
@@ -14,12 +14,13 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.dao;
+
+import org.apache.cloudstack.dns.vo.DnsZoneJoinVO;
import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+public interface DnsZoneJoinDao extends GenericDao<DnsZoneJoinVO, Long> {
+
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDaoImpl.java
similarity index 75%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDaoImpl.java
index 2ca901fa3e5..6be2a3a798e 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDaoImpl.java
@@ -14,12 +14,12 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.dao;
-import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
+import org.apache.cloudstack.dns.vo.DnsZoneJoinVO;
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+import com.cloud.utils.db.GenericDaoBase;
+
+public class DnsZoneJoinDaoImpl extends GenericDaoBase<DnsZoneJoinVO, Long> implements DnsZoneJoinDao {
}
diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java
similarity index 70%
copy from engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
copy to server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java
index 2ca901fa3e5..19c688ebc04 100644
--- a/engine/schema/src/main/java/com/cloud/vm/dao/NicDetailsDao.java
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java
@@ -14,12 +14,16 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-package com.cloud.vm.dao;
-import org.apache.cloudstack.resourcedetail.ResourceDetailsDao;
+package org.apache.cloudstack.dns.dao;
+
+import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO;
import com.cloud.utils.db.GenericDao;
-import com.cloud.vm.NicDetailVO;
-public interface NicDetailsDao extends GenericDao<NicDetailVO, Long>, ResourceDetailsDao<NicDetailVO> {
+public interface DnsZoneNetworkMapDao extends GenericDao<DnsZoneNetworkMapVO, Long> {
+ void removeNetworkMappingByZoneId(long dnsZoneId);
+ DnsZoneNetworkMapVO findByNetworkId(long networkId);
+
+ DnsZoneNetworkMapVO findByZoneId(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 00000000000..7dcae3c4e32
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.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.dao;
+
+import org.apache.cloudstack.api.ApiConstants;
+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<DnsZoneNetworkMapVO, Long> implements DnsZoneNetworkMapDao {
+ private final SearchBuilder<DnsZoneNetworkMapVO> ZoneNetworkSearch;
+ private final SearchBuilder<DnsZoneNetworkMapVO> NetworkSearch;
+
+ public DnsZoneNetworkMapDaoImpl() {
+ super();
+ ZoneNetworkSearch = createSearchBuilder();
+ ZoneNetworkSearch.and(ApiConstants.DNS_ZONE_ID, ZoneNetworkSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ);
+ ZoneNetworkSearch.done();
+
+ NetworkSearch = createSearchBuilder();
+ NetworkSearch.and(ApiConstants.NETWORK_ID, NetworkSearch.entity().getNetworkId(), SearchCriteria.Op.EQ);
+ NetworkSearch.done();
+ }
+
+ @Override
+ public void removeNetworkMappingByZoneId(long dnsZoneId) {
+ SearchCriteria<DnsZoneNetworkMapVO> sc = ZoneNetworkSearch.create();
+ sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId);
+ remove(sc);
+ }
+
+ @Override
+ public DnsZoneNetworkMapVO findByNetworkId(long networkId) {
+ SearchCriteria<DnsZoneNetworkMapVO> sc = NetworkSearch.create();
+ sc.setParameters(ApiConstants.NETWORK_ID, networkId);
+ return findOneBy(sc);
+ }
+
+ @Override
+ public DnsZoneNetworkMapVO findByZoneId(long dnsZoneId) {
+ SearchCriteria<DnsZoneNetworkMapVO> sc = ZoneNetworkSearch.create();
+ sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId);
+ return findOneBy(sc);
+ }
+}
diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/NicDnsJoinDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/NicDnsJoinDao.java
new file mode 100644
index 00000000000..682f1811812
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/NicDnsJoinDao.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.dao;
+
+import java.util.List;
+
+import org.apache.cloudstack.dns.vo.NicDnsJoinVO;
+
+import com.cloud.utils.db.GenericDao;
+
+public interface NicDnsJoinDao extends GenericDao<NicDnsJoinVO, Long> {
+
+ /**
+ * Used for Collision Checks.
+ *
+ * @param dnsZoneId
+ * @param nicDnsName
+ * @return active records to see who currently owns the dnsRecordUrl.
+ */
+ NicDnsJoinVO findActiveByDnsRecordAndZone(long dnsZoneId, String nicDnsName);
+
+ /**
+ * Used to sync DNS record url based on available ips for vmId in the dnsZone
+ *
+ * @param vmId
+ * @param dnsZoneId
+ * @param nicDnsName
+ * @return list of active nics using the dnsRecordUrl, supports null vmId for dnsZone wide query
+ */
+ List<NicDnsJoinVO> listActiveByVmIdZoneAndDnsRecord(Long vmId, long dnsZoneId, String nicDnsName);
+
+ /**
+ * Used for VM Start/Running
+ * @param vmId
+ * @return records associated to vmId
+ */
+ List<NicDnsJoinVO> listActiveByVmId(long vmId);
+
+ /**
+ * Used by Instance Destroy/Stop or NIC delete
+ * @param vmId
+ * @return records with soft-delete
+ */
+ List<NicDnsJoinVO> listIncludingRemovedByVmId(long vmId);
+
+ /**
+ * Find all records for dnsZoneId with valid nicDnsName
+ * @param dnsZoneId
+ * @return
+ */
+ List<NicDnsJoinVO> listByZoneId(long dnsZoneId);
+}
diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/NicDnsJoinDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/NicDnsJoinDaoImpl.java
new file mode 100644
index 00000000000..9e7652ef0d4
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/dns/dao/NicDnsJoinDaoImpl.java
@@ -0,0 +1,107 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.dns.dao;
+
+import java.util.List;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.dns.vo.NicDnsJoinVO;
+
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+
+public class NicDnsJoinDaoImpl extends GenericDaoBase<NicDnsJoinVO, Long> implements NicDnsJoinDao {
+ private final SearchBuilder<NicDnsJoinVO> activeDnsRecordZoneSearch;
+ private final SearchBuilder<NicDnsJoinVO> activeVmZoneDnsRecordSearch; // Route for null vmId
+ private final SearchBuilder<NicDnsJoinVO> activeVmSearch;
+ private final SearchBuilder<NicDnsJoinVO> activeDnsRecordsByZoneIdSearch;
+
+ public NicDnsJoinDaoImpl() {
+
+ activeDnsRecordZoneSearch = createSearchBuilder();
+ activeDnsRecordZoneSearch.and(ApiConstants.NIC_DNS_NAME, activeDnsRecordZoneSearch.entity().getNicDnsName(), SearchCriteria.Op.EQ);
+ activeDnsRecordZoneSearch.and(ApiConstants.DNS_ZONE_ID, activeDnsRecordZoneSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ);
+ activeDnsRecordZoneSearch.and(ApiConstants.REMOVED, activeDnsRecordZoneSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
+ activeDnsRecordZoneSearch.done();
+
+ activeVmZoneDnsRecordSearch = createSearchBuilder();
+ activeVmZoneDnsRecordSearch.and(ApiConstants.INSTANCE_ID, activeVmZoneDnsRecordSearch.entity().getInstanceId(), SearchCriteria.Op.EQ);
+ activeVmZoneDnsRecordSearch.and(ApiConstants.NIC_DNS_NAME, activeVmZoneDnsRecordSearch.entity().getNicDnsName(), SearchCriteria.Op.EQ);
+ activeVmZoneDnsRecordSearch.and(ApiConstants.DNS_ZONE_ID, activeVmZoneDnsRecordSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ);
+ activeVmZoneDnsRecordSearch.and(ApiConstants.REMOVED, activeVmZoneDnsRecordSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
+ activeVmZoneDnsRecordSearch.done();
+
+ activeVmSearch = createSearchBuilder();
+ activeVmSearch.and(ApiConstants.INSTANCE_ID, activeVmSearch.entity().getInstanceId(), SearchCriteria.Op.EQ);
+ activeVmSearch.done();
+
+ activeDnsRecordsByZoneIdSearch = createSearchBuilder();
+ activeDnsRecordsByZoneIdSearch.selectFields(activeDnsRecordsByZoneIdSearch.entity().getId());
+ activeDnsRecordsByZoneIdSearch.and(ApiConstants.DNS_ZONE_ID, activeDnsRecordsByZoneIdSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ);
+ activeDnsRecordsByZoneIdSearch.and(ApiConstants.NIC_DNS_NAME, activeDnsRecordsByZoneIdSearch.entity().getNicDnsName(), SearchCriteria.Op.NNULL);
+ activeDnsRecordsByZoneIdSearch.and(ApiConstants.REMOVED, activeDnsRecordsByZoneIdSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
+ activeDnsRecordsByZoneIdSearch.done();
+
+ }
+
+ @Override
+ public NicDnsJoinVO findActiveByDnsRecordAndZone(long dnsZoneId, String nicDnsName) {
+ SearchCriteria<NicDnsJoinVO> sc = activeDnsRecordZoneSearch.create();
+ sc.setParameters(ApiConstants.NIC_DNS_NAME, nicDnsName);
+ sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId);
+ return findOneBy(sc);
+ }
+
+ @Override
+ public List<NicDnsJoinVO> listActiveByVmIdZoneAndDnsRecord(Long vmId, long dnsZoneId, String nicDnsName) {
+ if (vmId != null) {
+ SearchCriteria<NicDnsJoinVO> sc = activeVmZoneDnsRecordSearch.create();
+ sc.setParameters(ApiConstants.INSTANCE_ID, vmId);
+ sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId);
+ sc.setParameters(ApiConstants.NIC_DNS_NAME, nicDnsName);
+ return listBy(sc);
+ } else {
+ SearchCriteria<NicDnsJoinVO> sc = activeDnsRecordZoneSearch.create();
+ sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId);
+ sc.setParameters(ApiConstants.NIC_DNS_NAME, nicDnsName);
+ return listBy(sc);
+ }
+ }
+
+ @Override
+ public List<NicDnsJoinVO> listActiveByVmId(long vmId) {
+ SearchCriteria<NicDnsJoinVO> sc = activeVmSearch.create();
+ sc.setParameters(ApiConstants.INSTANCE_ID, vmId);
+ return listBy(sc);
+ }
+
+ @Override
+ public List<NicDnsJoinVO> listIncludingRemovedByVmId(long vmId) {
+ SearchCriteria<NicDnsJoinVO> sc = activeVmSearch.create();
+ sc.setParameters(ApiConstants.INSTANCE_ID, vmId);
+ return listIncludingRemovedBy(sc);
+ }
+
+ @Override
+ public List<NicDnsJoinVO> listByZoneId(long dnsZoneId) {
+ SearchCriteria<NicDnsJoinVO> sc = activeDnsRecordsByZoneIdSearch.create();
+ sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId);
+ return listBy(sc);
+ }
+}
diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerDetailVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerDetailVO.java
new file mode 100644
index 00000000000..040e891a945
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerDetailVO.java
@@ -0,0 +1,99 @@
+// 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.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+import org.apache.cloudstack.api.ResourceDetail;
+
+@Entity
+@Table(name = "dns_server_details")
+public class DnsServerDetailVO implements ResourceDetail {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private long id;
+
+ @Column(name = "dns_server_id")
+ private long resourceId;
+
+ @Column(name = "name")
+ private String name;
+
+ @Column(name = "value")
+ private String value;
+
+ @Column(name = "display")
+ private boolean display = true;
+
+ protected DnsServerDetailVO() {
+ }
+
+ public DnsServerDetailVO(long dnsServerId, String name, String value, boolean display) {
+ this.resourceId = dnsServerId;
+ this.name = name;
+ this.value = value;
+ this.display = display;
+ }
+
+ @Override
+ public long getId() {
+ return id;
+ }
+
+ @Override
+ public long getResourceId() {
+ return resourceId;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean isDisplay() {
+ return display;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public void setResourceId(long resourceId) {
+ this.resourceId = resourceId;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+}
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 00000000000..ae59efc51c2
--- /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<String> 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/DnsServerVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java
new file mode 100644
index 00000000000..ff1e355671e
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java
@@ -0,0 +1,271 @@
+// 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.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+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 javax.persistence.Transient;
+
+import org.apache.cloudstack.dns.DnsProviderType;
+import org.apache.cloudstack.dns.DnsServer;
+
+import com.cloud.utils.StringUtils;
+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 = "port")
+ private Integer port;
+
+ @Column(name = "provider_type")
+ @Enumerated(EnumType.STRING)
+ private DnsProviderType providerType;
+
+ @Column(name = "dns_username")
+ private String dnsUserName;
+
+ @Encrypt
+ @Column(name = "dns_api_key")
+ private String dnsApiKey;
+
+ @Column(name = "is_public")
+ private boolean publicServer;
+
+ @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 = "domain_id")
+ private long domainId;
+
+ @Column(name = "name_servers")
+ private String nameServers;
+
+ @Column(name = GenericDao.CREATED_COLUMN)
+ @Temporal(value = TemporalType.TIMESTAMP)
+ private Date created;
+
+ @Column(name = GenericDao.REMOVED_COLUMN)
+ @Temporal(value = TemporalType.TIMESTAMP)
+ private Date removed = null;
+
+ @Transient
+ Map<String, String> details;
+
+ DnsServerVO() {
+ this.uuid = UUID.randomUUID().toString();
+ this.created = new Date();
+ }
+
+ public DnsServerVO(String name, String url, Integer port, DnsProviderType providerType, String dnsUserName, String dnsApiKey,
+ boolean isPublic, String publicDomainSuffix, List<String> nameServers, Long accountId, Long domainId) {
+ this();
+ this.name = name;
+ this.url = url;
+ this.port = port;
+ this.providerType = providerType;
+ this.dnsUserName = dnsUserName;
+ this.dnsApiKey = dnsApiKey;
+ this.accountId = accountId;
+ this.domainId = domainId;
+ this.publicDomainSuffix = publicDomainSuffix;
+ this.publicServer = isPublic;
+ this.state = State.Enabled;
+ this.nameServers = String.join(",", nameServers);
+ }
+
+ @Override
+ public long getId() {
+ return id;
+ }
+
+ @Override
+ public Class<?> getEntityType() {
+ return DnsServer.class;
+ }
+
+ @Override
+ public String getName() {
+ return name;
... 5869 lines suppressed ...