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 ...