1. Setup Dns zone schema

2. added relevant changes in dao and vo
3. worked on creatednszone, integration with mgr
4. powerdns create zone api call
This commit is contained in:
Manoj Kumar 2026-02-13 13:52:08 +05:30
parent 9911c280e1
commit df2131810f
No known key found for this signature in database
GPG Key ID: E952B7234D2C6F88
12 changed files with 293 additions and 54 deletions

View File

@ -23,7 +23,7 @@ import com.cloud.exception.ResourceAllocationException;
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class CreateDnsZoneCmd extends BaseAsyncCreateCmd {
private static final String s_name = "creatednszoneresponse";
private static final String COMMAND_RESPONSE_NAME = "creatednszoneresponse";
@Inject
DnsProviderManager dnsProviderManager;
@ -48,8 +48,8 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd {
description = "The type of zone (Public, Private). Defaults to Public.")
private String type;
// Standard CloudStack ownership parameters (account/domain) are handled
// automatically by the BaseCmd parent if we access them via getEntityOwnerId()
@Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "Display text for the zone")
private String description;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
@ -71,16 +71,18 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd {
return type;
}
public String getDescription() {
return description;
}
/////////////////////////////////////////////////////
/////////////// Implementation //////////////////////
/////////////////////////////////////////////////////
@Override
public void create() throws ResourceAllocationException {
// Phase 1: DB Persist
// The manager should create the DnsZoneVO in 'Allocating' state
try {
DnsZone zone = dnsProviderManager.allocDnsZone(this);
DnsZone zone = dnsProviderManager.allocateDnsZone(this);
if (zone != null) {
setEntityId(zone.getId());
setEntityUuid(zone.getUuid());
@ -94,12 +96,8 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd {
@Override
public void execute() {
// Phase 2: Action (Call Plugin)
// The manager should retrieve the zone by ID, call the plugin, and update state to 'Ready'
try {
// Note: We use getEntityId() which was set in the create() phase
DnsZone result = dnsProviderManager.provisionDnsZone(getEntityId());
if (result != null) {
DnsZoneResponse response = dnsProviderManager.createDnsZoneResponse(result);
response.setResponseName(getCommandName());
@ -114,7 +112,7 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd {
@Override
public String getCommandName() {
return s_name;
return COMMAND_RESPONSE_NAME;
}
@Override
@ -124,7 +122,7 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd {
@Override
public String getEventType() {
return EventTypes.EVENT_DNS_ZONE_CREATE; // You must add this constant to EventTypes.java
return EventTypes.EVENT_DNS_ZONE_CREATE;
}
@Override

View File

@ -28,35 +28,67 @@ import com.google.gson.annotations.SerializedName;
@EntityReference(value = DnsZone.class)
public class DnsZoneResponse extends BaseResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "the ID of the DNS zone")
@Param(description = "ID of the DNS zone")
private String id;
@SerializedName(ApiConstants.NAME)
@Param(description = "the name of the DNS zone")
@Param(description = "Name of the DNS zone")
private String name;
@SerializedName("dnsserverid")
@Param(description = "the ID of the DNS server this zone belongs to")
private String dnsServerId;
@Param(description = "ID of the DNS server this zone belongs to")
private Long dnsServerId;
@SerializedName("dnsservername")
@Param(description = "the name of the DNS server this zone belongs to")
@Param(description = "Name of the DNS server this zone belongs to")
private String dnsServerName;
@SerializedName(ApiConstants.NETWORK_ID)
@Param(description = "the ID of the network this zone is associated with")
@Param(description = "ID of the network this zone is associated with")
private String networkId;
@SerializedName(ApiConstants.NETWORK_NAME)
@Param(description = "the name of the network this zone is associated with")
@Param(description = "Name of the network this zone is associated with")
private String networkName;
@SerializedName(ApiConstants.TYPE)
@Param(description = "the type of the zone (Public/Private)")
private String type;
@Param(description = "The type of the zone (Public/Private)")
private DnsZone.ZoneType type;
@SerializedName(ApiConstants.STATE)
@Param(description = "The state of the zone (Active/Inactive)")
private DnsZone.State state;
public DnsZoneResponse() {
super();
setObjectName("dnszone");
}
public void setName(String name) {
this.name = name;
}
public void setDnsServerId(Long dnsServerId) {
this.dnsServerId = dnsServerId;
}
public void setDnsServerName(String dnsServerName) {
this.dnsServerName = dnsServerName;
}
public void setNetworkId(String networkId) {
this.networkId = networkId;
}
public void setNetworkName(String networkName) {
this.networkName = networkName;
}
public void setType(DnsZone.ZoneType type) {
this.type = type;
}
public void setState(DnsZone.State state) {
this.state = state;
}
}

View File

@ -28,10 +28,9 @@ public interface DnsProvider extends Adapter {
boolean validate(DnsServer server) throws Exception;
// Zone Operations
boolean createZone(DnsServer server, DnsZone zone);
boolean provisionZone(DnsServer server, DnsZone zone) throws Exception;
boolean deleteZone(DnsServer server, DnsZone zone);
DnsRecord createRecord(DnsServer server, DnsZone zone, DnsRecord record);
boolean updateRecord(DnsServer server, DnsZone zone, DnsRecord record);
boolean deleteRecord(DnsServer server, DnsZone zone, DnsRecord record);

View File

@ -60,10 +60,10 @@ public interface DnsProviderManager extends Manager, PluggableService {
List<String> listProviderNames();
// Allocates the DB row (State: Allocating)
DnsZone allocDnsZone(CreateDnsZoneCmd cmd);
// Allocates the DB row (State: Inactive)
DnsZone allocateDnsZone(CreateDnsZoneCmd cmd);
// Calls the Plugin (State: Allocating -> Ready/Error)
// Calls the Plugin (State: Inactive -> Active)
DnsZone provisionDnsZone(long zoneId);
// Helper to create the response object

View File

@ -19,10 +19,11 @@ package org.apache.cloudstack.dns;
import java.util.List;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
public interface DnsZone extends InternalIdentity, Identity {
public interface DnsZone extends InternalIdentity, Identity, ControlledEntity {
enum ZoneType {
Public, Private
}
@ -38,5 +39,9 @@ public interface DnsZone extends InternalIdentity, Identity {
ZoneType getType();
String getDescription();
List<Long> getAssociatedNetworks();
State getState();
}

View File

@ -58,7 +58,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` (
-- 1. DNS Server Table (Stores DNS Server Configurations)
CREATE TABLE `cloud`.`dns_server` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns server',
`uuid` varchar(255) COMMENT 'uuid of the dns server',
`uuid` varchar(40) COMMENT 'uuid of the dns server',
`name` varchar(255) NOT NULL COMMENT 'display name of the dns server',
`provider_type` varchar(255) NOT NULL COMMENT 'Provider type such as PowerDns',
`url` varchar(1024) NOT NULL COMMENT 'dns server url',
@ -79,16 +79,25 @@ CREATE TABLE `cloud`.`dns_server` (
-- 2. DNS Zone Table (Stores DNS Zone Metadata)
CREATE TABLE `cloud`.`dns_zone` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone',
`uuid` varchar(255) COMMENT 'uuid of the dns zone',
`uuid` varchar(40) COMMENT 'uuid of the dns zone',
`name` varchar(255) NOT NULL COMMENT 'dns zone name (e.g. example.com)',
`dns_server_id` bigint unsigned NOT NULL COMMENT 'fk to dns_server.id',
`external_reference` VARCHAR(255) COMMENT 'id of external provider resource',
`domain_id` bigint unsigned COMMENT 'for domain-specific ownership',
`account_id` bigint unsigned COMMENT 'account id. foreign key to account table',
`description` varchar(1024) DEFAULT NULL,
`type` ENUM('Private', 'Public') NOT NULL DEFAULT 'Public',
`state` ENUM('Active', 'Inactive') NOT NULL DEFAULT 'Inactive',
`created` datetime NOT NULL COMMENT 'date created',
`removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)',
PRIMARY KEY (`id`),
CONSTRAINT `uc_dns_zone__uuid` UNIQUE (`uuid`),
CONSTRAINT `uc_dns_zone__name_server_type` UNIQUE (`name`, `dns_server_id`, `type`),
KEY `i_dns_zone__dns_server` (`dns_server_id`),
CONSTRAINT `fk_dns_zone__dns_server_id` FOREIGN KEY (`dns_server_id`) REFERENCES `dns_server` (`id`) ON DELETE CASCADE
KEY `i_dns_zone__account_id` (`account_id`),
CONSTRAINT `fk_dns_zone__dns_server_id` FOREIGN KEY (`dns_server_id`) REFERENCES `dns_server` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_dns_zone__account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_dns_zone__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. DNS Zone Network Map (One-to-Many Link)

View File

@ -18,11 +18,14 @@
package org.apache.cloudstack.dns.powerdns;
import java.io.IOException;
import java.util.List;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
@ -32,6 +35,8 @@ import org.slf4j.LoggerFactory;
import com.cloud.utils.exception.CloudRuntimeException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class PowerDnsClient implements AutoCloseable {
public static final Logger logger = LoggerFactory.getLogger(PowerDnsClient.class);
@ -40,19 +45,7 @@ public class PowerDnsClient implements AutoCloseable {
private final CloseableHttpClient httpClient;
public void validate(String baseUrl, String apiKey) {
String normalizedUrl = baseUrl.trim();
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
normalizedUrl = "http://" + normalizedUrl; // default to HTTP
}
if (normalizedUrl.endsWith("/")) {
normalizedUrl = normalizedUrl.substring(0, normalizedUrl.length() - 1);
}
String checkUrl = normalizedUrl + "/api/v1/servers";
String checkUrl = buildApiUrl(baseUrl, "/api/v1/servers");
HttpGet request = new HttpGet(checkUrl);
request.addHeader("X-API-Key", apiKey);
request.addHeader("Accept", "application/json");
@ -94,6 +87,62 @@ public class PowerDnsClient implements AutoCloseable {
}
}
public void createZone(String baseUrl, String apiKey, String zoneName, List<String> nameservers) {
String url = buildApiUrl(baseUrl, "/servers/localhost/zones");
ObjectNode json = MAPPER.createObjectNode();
json.put("name", zoneName.endsWith(".") ? zoneName : zoneName + ".");
json.put("kind", "Native");
json.put("dnssec", false);
if (nameservers != null && !nameservers.isEmpty()) {
ArrayNode nsArray = json.putArray("nameservers");
for (String ns : nameservers) {
nsArray.add(ns.endsWith(".") ? ns : ns + ".");
}
}
logger.debug("Creating PowerDNS zone: {} using URL: {}", zoneName, url);
HttpPost request = new HttpPost(url);
request.addHeader("X-API-Key", apiKey);
request.addHeader("Content-Type", "application/json");
request.addHeader("Accept", "application/json");
try {
request.setEntity(new StringEntity(json.toString()));
try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
String body = response.getEntity() != null
? EntityUtils.toString(response.getEntity())
: null;
if (statusCode == HttpStatus.SC_CREATED) {
logger.debug("Zone {} created successfully", zoneName);
return;
}
if (statusCode == HttpStatus.SC_CONFLICT) {
throw new CloudRuntimeException("Zone already exists: " + zoneName);
}
if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
statusCode == HttpStatus.SC_FORBIDDEN) {
throw new CloudRuntimeException("Invalid PowerDNS API key");
}
logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body);
throw new CloudRuntimeException(String.format("Failed to create zone %s (HTTP %d)", zoneName, statusCode));
}
} catch (IOException e) {
throw new CloudRuntimeException("Error while creating PowerDNS zone " + zoneName, e);
}
}
public PowerDnsClient() {
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(TIMEOUT_MS)
@ -107,6 +156,24 @@ public class PowerDnsClient implements AutoCloseable {
.build();
}
private String normalizeBaseUrl(String baseUrl) {
if (baseUrl == null) {
throw new IllegalArgumentException("PowerDNS base URL cannot be null");
}
String normalizedUrl = baseUrl.trim();
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
normalizedUrl = "http://" + normalizedUrl;
}
if (normalizedUrl.endsWith("/")) {
normalizedUrl = normalizedUrl.substring(0, normalizedUrl.length() - 1);
}
return normalizedUrl;
}
private String buildApiUrl(String baseUrl, String path) {
return normalizeBaseUrl(baseUrl) + "/api/v1" + path;
}
@Override
public void close() {
try {

View File

@ -51,8 +51,20 @@ public class PowerDnsProvider extends AdapterBase implements DnsProvider {
}
@Override
public boolean createZone(DnsServer server, DnsZone zone) {
return false;
public boolean provisionZone(DnsServer server, DnsZone zone) throws Exception {
if (StringUtils.isBlank(zone.getName())) {
throw new IllegalArgumentException("Zone name cannot be empty");
}
if (StringUtils.isBlank(server.getUrl())) {
throw new IllegalArgumentException("PowerDNS API URL cannot be empty");
}
if (StringUtils.isBlank(server.getApiKey())) {
throw new IllegalArgumentException("PowerDNS API key cannot be empty");
}
client.createZone(server.getUrl(), server.getApiKey(), zone.getName(), null);
return true;
}
@Override

View File

@ -39,7 +39,9 @@ import org.apache.cloudstack.api.response.DnsZoneResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.dns.dao.DnsServerDao;
import org.apache.cloudstack.dns.dao.DnsZoneDao;
import org.apache.cloudstack.dns.vo.DnsServerVO;
import org.apache.cloudstack.dns.vo.DnsZoneVO;
import org.springframework.stereotype.Component;
import com.cloud.exception.InvalidParameterValueException;
@ -59,6 +61,8 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
AccountManager accountMgr;
@Inject
DnsServerDao dnsServerDao;
@Inject
DnsZoneDao dnsZoneDao;
private DnsProvider getProvider(DnsProviderType type) {
if (type == null) {
@ -272,18 +276,67 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
}
@Override
public DnsZone allocDnsZone(CreateDnsZoneCmd cmd) {
return null;
public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) {
Account caller = CallContext.current().getCallingAccount();
DnsServerVO server = dnsServerDao.findById(cmd.getDnsServerId());
if (server == null) {
throw new InvalidParameterValueException("DNS Server not found");
}
boolean isOwner = (server.getAccountId() == caller.getId());
if (!server.isPublic() && !isOwner) {
throw new PermissionDeniedException("You do not have permission to use this DNS Server.");
}
DnsZone.ZoneType type = DnsZone.ZoneType.Public;
if (cmd.getType() != null) {
try {
type = DnsZone.ZoneType.valueOf(cmd.getType());
} catch (IllegalArgumentException e) {
throw new InvalidParameterValueException("Invalid Zone Type");
}
}
DnsZoneVO existing = dnsZoneDao.findByNameServerAndType(cmd.getName(), server.getId(), type);
if (existing != null) {
throw new InvalidParameterValueException("Zone already exists on this server.");
}
DnsZoneVO dnsZoneVO = new DnsZoneVO(cmd.getName(), type, server.getId(), caller.getId(), caller.getDomainId(), cmd.getDescription());
return dnsZoneDao.persist(dnsZoneVO);
}
@Override
public DnsZone provisionDnsZone(long zoneId) {
return null;
DnsZoneVO dnsZone = dnsZoneDao.findById(zoneId);
if (dnsZone == null) {
throw new CloudRuntimeException("DNS Zone not found during provisioning");
}
DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId());
try {
DnsProvider provider = getProvider(server.getProviderType());
logger.debug("Provision DNS zone: {} on DNS server: {}", dnsZone.getName(), server.getName());
boolean success = provider.provisionZone(server, dnsZone);
if (success) {
dnsZone.setState(DnsZone.State.Active);
dnsZoneDao.update(dnsZone.getId(), dnsZone);
return dnsZone;
} else {
logger.error("DNS provider failed to provision zone: {}", dnsZone.getName());
throw new CloudRuntimeException("DNS provider failed to provision zone");
}
} catch (Exception ex) {
logger.error("Failed to provision zone: {} on server: {}", dnsZone.getName(), server.getName(), ex);
dnsZoneDao.remove(zoneId);
throw new CloudRuntimeException("Failed to provision zone: " + dnsZone.getName());
}
}
@Override
public DnsZoneResponse createDnsZoneResponse(DnsZone zone) {
return null;
DnsZoneResponse res = new DnsZoneResponse();
res.setName(zone.getName());
res.setDnsServerId(zone.getDnsServerId());
res.setType(zone.getType());
res.setState(zone.getState());
return res;
}
@Override

View File

@ -19,10 +19,13 @@ 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.db.GenericDao;
public interface DnsZoneDao extends GenericDao<DnsZoneVO, Long> {
List<DnsZoneVO> listByServerId(long serverId);
List<DnsZoneVO> listByAccount(long accountId);
DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type);
}

View File

@ -19,6 +19,8 @@ package org.apache.cloudstack.dns.dao;
import java.util.List;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.dns.DnsZone;
import org.apache.cloudstack.dns.vo.DnsZoneVO;
import org.springframework.stereotype.Component;
@ -30,12 +32,24 @@ import com.cloud.utils.db.SearchCriteria;
public class DnsZoneDaoImpl extends GenericDaoBase<DnsZoneVO, Long> implements DnsZoneDao {
static final String DNS_SERVER_ID = "dnsServerId";
SearchBuilder<DnsZoneVO> ServerSearch;
SearchBuilder<DnsZoneVO> AccountSearch;
SearchBuilder<DnsZoneVO> NameServerTypeSearch;
public DnsZoneDaoImpl() {
super();
ServerSearch = createSearchBuilder();
ServerSearch.and(DNS_SERVER_ID, ServerSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ);
ServerSearch.done();
AccountSearch = createSearchBuilder();
AccountSearch.and(ApiConstants.ACCOUNT_ID, AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
AccountSearch.done();
NameServerTypeSearch = createSearchBuilder();
NameServerTypeSearch.and(ApiConstants.NAME, NameServerTypeSearch.entity().getName(), SearchCriteria.Op.EQ);
NameServerTypeSearch.and(DNS_SERVER_ID, NameServerTypeSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ);
NameServerTypeSearch.and(ApiConstants.TYPE, NameServerTypeSearch.entity().getType(), SearchCriteria.Op.EQ);
NameServerTypeSearch.done();
}
@Override
@ -44,4 +58,20 @@ public class DnsZoneDaoImpl extends GenericDaoBase<DnsZoneVO, Long> implements D
sc.setParameters(DNS_SERVER_ID, serverId);
return listBy(sc);
}
@Override
public List<DnsZoneVO> listByAccount(long accountId) {
SearchCriteria<DnsZoneVO> sc = AccountSearch.create();
sc.setParameters(ApiConstants.ACCOUNT_ID, accountId);
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(DNS_SERVER_ID, dnsServerId);
sc.setParameters(ApiConstants.TYPE, type);
return findOneBy(sc);
}
}

View File

@ -53,6 +53,15 @@ public class DnsZoneVO implements DnsZone {
@Column(name = "dns_server_id")
private long dnsServerId;
@Column(name = "account_id")
private long accountId;
@Column(name = "domain_id")
private long domainId;
@Column(name = "description")
private String description;
@Column(name = "external_reference")
private String externalReference;
@ -64,9 +73,6 @@ public class DnsZoneVO implements DnsZone {
@Enumerated(EnumType.STRING)
private State state;
@Column(name = "account_id")
private long accountId;
@Column(name = GenericDao.CREATED_COLUMN)
@Temporal(value = TemporalType.TIMESTAMP)
private Date created = null;
@ -78,14 +84,22 @@ public class DnsZoneVO implements DnsZone {
public DnsZoneVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
this.state = State.Inactive;
}
public DnsZoneVO(String name, long dnsServerId, long accountId) {
public DnsZoneVO(String name, ZoneType type, long dnsServerId, long accountId, long domainId, String description) {
this();
this.name = name;
this.type = (type != null) ? type : ZoneType.Public;
this.dnsServerId = dnsServerId;
this.accountId = accountId;
this.type = ZoneType.Public;
this.domainId = domainId;
this.description = description;
}
@Override
public Class<?> getEntityType() {
return DnsZone.class;
}
@Override
@ -108,11 +122,21 @@ public class DnsZoneVO implements DnsZone {
return type;
}
@Override
public String getDescription() {
return description;
}
@Override
public List<Long> getAssociatedNetworks() {
return List.of();
}
@Override
public State getState() {
return state;
}
@Override
public String getUuid() {
return uuid;
@ -122,4 +146,11 @@ public class DnsZoneVO implements DnsZone {
public long getId() {
return id;
}
@Override
public long getDomainId() {
return domainId;
}
public void setState(State state) { this.state = state; }
}