Tested following flow:

1. Add dns server
2. create zone
3. add records
4. verify in powerdns
5. verify using dig
This commit is contained in:
Manoj Kumar 2026-02-16 16:40:19 +05:30
parent f29b8be24d
commit 9b77c3708e
No known key found for this signature in database
GPG Key ID: E952B7234D2C6F88
15 changed files with 429 additions and 83 deletions

View File

@ -1336,6 +1336,8 @@ public class ApiConstants {
// DNS provider related
public static final String NAME_SERVERS = "nameservers";
public static final String CREDENTIALS = "credentials";
public static final String DNS_ZONE_ID = "dnszoneid";
public static final String CONTENTS = "contents";
public static final String PUBLIC_DOMAIN_SUFFIX = "publicdomainsuffix";
public static final String PARAMETER_DESCRIPTION_ACTIVATION_RULE = "Quota tariff's activation rule. It can receive a JS script that results in either " +

View File

@ -17,6 +17,8 @@
package org.apache.cloudstack.api.command.user.dns;
import java.util.List;
import javax.inject.Inject;
import org.apache.cloudstack.api.APICommand;
@ -51,7 +53,7 @@ public class AddDnsServerCmd extends BaseCmd {
@Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)")
private String provider;
@Parameter(name = ApiConstants.CREDENTIALS, type = CommandType.STRING, required = false, description = "API Key or Credentials for the external provider")
@Parameter(name = ApiConstants.CREDENTIALS, type = CommandType.STRING, description = "API Key or Credentials for the external provider")
private String credentials;
@Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server")
@ -63,8 +65,9 @@ public class AddDnsServerCmd extends BaseCmd {
@Parameter(name = ApiConstants.PUBLIC_DOMAIN_SUFFIX, type = CommandType.STRING, description = "The domain suffix used for public access (e.g. public.example.com)")
private String publicDomainSuffix;
@Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.STRING, description = "Comma separated list of name servers")
private String nameServers;
@Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.LIST, collectionType = CommandType.STRING,
required = true, description = "Comma separated list of name servers")
private List<String> nameServers;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
@ -89,7 +92,7 @@ public class AddDnsServerCmd extends BaseCmd {
return publicDomainSuffix;
}
public String getNameServers() {
public List<String> getNameServers() {
return nameServers;
}

View File

@ -1,5 +1,7 @@
package org.apache.cloudstack.api.command.user.dns;
import java.util.List;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
@ -9,15 +11,19 @@ import org.apache.cloudstack.api.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)
public class CreateDnsRecordCmd extends BaseAsyncCmd {
@Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true)
private Long zoneId;
@Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true,
description = "ID of the DNS zone")
private Long dnsZoneId;
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "Record name")
private String name;
@ -25,19 +31,28 @@ public class CreateDnsRecordCmd extends BaseAsyncCmd {
@Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "Record type (A, CNAME)")
private String type;
@Parameter(name = "content", type = CommandType.STRING, required = true, description = "IP or target")
private String content;
@Parameter(name = ApiConstants.CONTENTS, type = CommandType.LIST, collectionType = CommandType.STRING, required = true,
description = "The content of the record (IP address for A/AAAA, FQDN for CNAME/NS, quoted string for TXT, etc.)")
private List<String> contents;
@Parameter(name = "ttl", type = CommandType.INTEGER, description = "Time to live")
private Integer ttl;
// Getters
public Long getZoneId() { return zoneId; }
public Long getDnsZoneId() { return dnsZoneId; }
public String getName() { return name; }
public String getType() { return type; }
public String getContent() { return content; }
public List<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 {

View File

@ -9,16 +9,19 @@ 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)
public class DeleteDnsRecordCmd extends BaseAsyncCmd {
@Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class,
@Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class,
required = true, description = "The ID of the DNS zone")
private Long zoneId;
private Long dnsZoneId;
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true)
private String name;
@ -27,9 +30,15 @@ public class DeleteDnsRecordCmd extends BaseAsyncCmd {
private String type;
// Getters
public Long getZoneId() { return zoneId; }
public DnsRecord.RecordType getType() {
DnsRecord.RecordType dnsRecordType = EnumUtils.getEnumIgnoreCase(DnsRecord.RecordType.class, type);
if (dnsRecordType == null) {
throw new InvalidParameterValueException("Invalid value passed for record type, valid values are: " + EnumUtils.listValues(DnsRecord.RecordType.values()));
}
return dnsRecordType;
}
public Long getDnsZoneId() { return dnsZoneId; }
public String getName() { return name; }
public String getType() { return type; }
@Override
public void execute() {

View File

@ -12,12 +12,13 @@ import org.apache.cloudstack.api.response.ListResponse;
responseObject = DnsRecordResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class ListDnsRecordsCmd extends BaseListCmd {
@Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class,
required = true, description = "the ID of the DNS zone to list records from")
private Long zoneId;
public Long getZoneId() {
return zoneId;
@Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true,
description = "ID of the DNS zone to list records from")
private Long dnsZoneId;
public Long getDnsZoneId() {
return dnsZoneId;
}
@Override

View File

@ -30,6 +30,9 @@ import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.dns.DnsProviderManager;
import org.apache.cloudstack.dns.DnsServer;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import com.cloud.utils.EnumUtils;
@APICommand(name = "updateDnsServer", description = "Update DNS server",
responseObject = DnsServerResponse.class, requestHasSensitiveInfo = true)
@ -67,6 +70,9 @@ public class UpdateDnsServerCmd extends BaseCmd {
@Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.STRING, description = "Comma separated list of name servers")
private String nameServers;
@Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "Update state for the DNS server (Enabled, Disabled)")
private String state;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -109,4 +115,15 @@ public class UpdateDnsServerCmd extends BaseCmd {
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;
}
}

View File

@ -17,6 +17,8 @@
package org.apache.cloudstack.api.response;
import java.util.List;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.dns.DnsRecord;
@ -27,29 +29,21 @@ import com.google.gson.annotations.SerializedName;
public class DnsRecordResponse extends BaseResponse {
@SerializedName(ApiConstants.NAME)
@Param(description = "the name of the DNS record")
@Param(description = "The record name (e.g., www.example.com.)")
private String name;
@SerializedName(ApiConstants.TYPE)
@Param(description = "the type of the DNS record (A, CNAME, etc)")
private String type;
@Param(description = "The record type (e.g., A, CNAME, TXT)")
private DnsRecord.RecordType type;
@SerializedName("content")
@Param(description = "the content of the record (IP address or target)")
private String content;
@SerializedName("contents")
@Param(description = "The contents of the record (IP address or target)")
private List<String> contents;
@SerializedName("ttl")
@Param(description = "the time to live (TTL) in seconds")
@Param(description = "Time to live (TTL) in seconds")
private Integer ttl;
@SerializedName(ApiConstants.ZONE_ID)
@Param(description = "the ID of the zone this record belongs to")
private String zoneId;
@SerializedName("sourceid")
@Param(description = "the external ID of the record on the provider")
private String sourceId;
public DnsRecordResponse() {
super();
setObjectName("dnsrecord");
@ -57,15 +51,7 @@ public class DnsRecordResponse extends BaseResponse {
// Setters
public void setName(String name) { this.name = name; }
// Accepts String or Enum.toString()
public void setType(String type) { this.type = type; }
public void setType(DnsRecord.RecordType type) {
this.type = (type != null) ? type.name() : null;
}
public void setContent(String content) { this.content = content; }
public void setType(DnsRecord.RecordType type) { this.type = type; }
public void setContent(List<String> contents) { this.contents = contents; }
public void setTtl(Integer ttl) { this.ttl = ttl; }
public void setZoneId(String zoneId) { this.zoneId = zoneId; }
public void setSourceId(String sourceId) { this.sourceId = sourceId; }
}

View File

@ -31,9 +31,8 @@ public interface DnsProvider extends Adapter {
void provisionZone(DnsServer server, DnsZone zone);
void deleteZone(DnsServer server, DnsZone zone) ;
DnsRecord createRecord(DnsServer server, DnsZone zone, DnsRecord record);
boolean updateRecord(DnsServer server, DnsZone zone, DnsRecord record);
boolean deleteRecord(DnsServer server, DnsZone zone, DnsRecord record);
void addRecord(DnsServer server, DnsZone zone, DnsRecord record);
List<DnsRecord> listRecords(DnsServer server, DnsZone zone);
void updateRecord(DnsServer server, DnsZone zone, DnsRecord record);
void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record);
}

View File

@ -56,12 +56,10 @@ public interface DnsProviderManager extends Manager, PluggableService {
DnsZone updateDnsZone(UpdateDnsZoneCmd cmd);
boolean deleteDnsZone(Long id);
ListResponse<DnsZoneResponse> listDnsZones(ListDnsZonesCmd cmd);
DnsZone getDnsZone(long id);
DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd);
boolean deleteDnsRecord(DeleteDnsRecordCmd cmd);
ListResponse<DnsRecordResponse> listDnsRecords(ListDnsRecordsCmd cmd);
List<String> listProviderNames();
@ -69,4 +67,5 @@ public interface DnsProviderManager extends Manager, PluggableService {
// Helper to create the response object
DnsZoneResponse createDnsZoneResponse(DnsZone zone);
DnsRecordResponse createDnsRecordResponse(DnsRecord record);
}

View File

@ -17,6 +17,8 @@
package org.apache.cloudstack.dns;
import java.util.List;
import com.cloud.utils.exception.CloudRuntimeException;
public class DnsRecord {
@ -36,16 +38,16 @@ public class DnsRecord {
}
private String name;
private RecordType type; // Enforced Enum here
private String content;
private RecordType type;
private List<String> contents;
private int ttl;
public DnsRecord() {}
public DnsRecord(String name, RecordType type, String content, int ttl) {
public DnsRecord(String name, RecordType type, List<String> contents, int ttl) {
this.name = name;
this.type = type;
this.content = content;
this.contents = contents;
this.ttl = ttl;
}
@ -56,8 +58,8 @@ public class DnsRecord {
public RecordType getType() { return type; }
public void setType(RecordType type) { this.type = type; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public List<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; }

View File

@ -24,7 +24,7 @@ import org.apache.cloudstack.api.InternalIdentity;
public interface DnsServer extends InternalIdentity, Identity {
enum State {
Enabled, Disabled;
Enabled, Disabled
};
String getName();
@ -33,6 +33,8 @@ public interface DnsServer extends InternalIdentity, Identity {
DnsProviderType getProviderType();
String getNameServers();
String getApiKey();
long getAccountId();

View File

@ -20,13 +20,18 @@ package org.apache.cloudstack.dns.powerdns;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.collections.CollectionUtils;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
@ -35,6 +40,7 @@ import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.cloud.utils.StringUtils;
import com.cloud.utils.exception.CloudRuntimeException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -89,8 +95,8 @@ public class PowerDnsClient implements AutoCloseable {
}
}
public void createZone(String baseUrl, String apiKey, String zoneName, List<String> nameservers) {
String normalizedZone = zoneName.endsWith(".") ? zoneName : zoneName + ".";
public void createZone(String baseUrl, String apiKey, String zoneName, String nameServers) {
String normalizedZone = formatZoneName(zoneName);
try {
String url = buildApiUrl(baseUrl, "/servers/localhost/zones");
ObjectNode json = MAPPER.createObjectNode();
@ -98,13 +104,15 @@ public class PowerDnsClient implements AutoCloseable {
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 + ".");
if (StringUtils.isNotEmpty(nameServers)) {
List<String> nsNames = new ArrayList<>(Arrays.asList(nameServers.split(",")));
if (!CollectionUtils.isEmpty(nsNames)) {
ArrayNode nsArray = json.putArray("nameservers");
for (String ns : nsNames) {
nsArray.add(ns.endsWith(".") ? ns : ns + ".");
}
}
}
HttpPost request = new HttpPost(url);
request.addHeader("X-API-Key", apiKey);
request.addHeader("Content-Type", "application/json");
@ -114,9 +122,7 @@ public class PowerDnsClient implements AutoCloseable {
try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
String body = response.getEntity() != null
? EntityUtils.toString(response.getEntity())
: null;
String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null;
if (statusCode == HttpStatus.SC_CREATED) {
logger.debug("Zone {} created successfully", zoneName);
@ -140,7 +146,7 @@ public class PowerDnsClient implements AutoCloseable {
}
public void deleteZone(String baseUrl, String apiKey, String zoneName) {
String normalizedZone = zoneName.endsWith(".") ? zoneName : zoneName + ".";
String normalizedZone = formatZoneName(zoneName);
try {
String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone);
@ -176,6 +182,155 @@ public class PowerDnsClient implements AutoCloseable {
}
}
public void modifyRecord(String baseUrl, String apiKey, String zoneName, String recordName, String type, long ttl, List<String> contents, String changeType) {
String normalizedZone = formatZoneName(zoneName);
String normalizedRecord = formatRecordName(recordName, zoneName);
try {
String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone);
ObjectNode root = MAPPER.createObjectNode();
ArrayNode rrsets = root.putArray("rrsets");
ObjectNode rrset = rrsets.addObject();
rrset.put("name", normalizedRecord);
rrset.put("type", type.toUpperCase());
rrset.put("ttl", ttl);
rrset.put("changetype", changeType);
ArrayNode records = rrset.putArray("records");
if (!CollectionUtils.isEmpty(contents)) {
for (String content : contents) {
ObjectNode record = records.addObject();
record.put("content", content);
record.put("disabled", false);
}
}
HttpPatch request = new HttpPatch(url);
request.addHeader("X-API-Key", apiKey);
request.addHeader("Content-Type", "application/json");
request.addHeader("Accept", "application/json");
request.setEntity(new StringEntity(root.toString()));
try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null;
if (statusCode == HttpStatus.SC_NO_CONTENT) {
logger.debug("Record {} {} added/updated in zone {}", normalizedRecord, type, normalizedZone);
return;
}
if (statusCode == HttpStatus.SC_NOT_FOUND) {
throw new CloudRuntimeException("Zone not found: " + normalizedZone);
}
if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) {
throw new CloudRuntimeException("Invalid PowerDNS API key");
}
logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body);
throw new CloudRuntimeException("Failed to add/update record " + normalizedRecord);
}
} catch (IOException e) {
throw new CloudRuntimeException("Error while adding PowerDNS record", e);
}
}
public void deleteRecord(String baseUrl, String apiKey, String zoneName, String recordName, String type) {
String normalizedZone = formatZoneName(zoneName);
String normalizedRecord = formatRecordName(recordName, zoneName);
try {
String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone);
ObjectNode root = MAPPER.createObjectNode();
ArrayNode rrsets = root.putArray("rrsets");
ObjectNode rrset = rrsets.addObject();
rrset.put("name", normalizedRecord);
rrset.put("type", type.toUpperCase());
rrset.put("changetype", "DELETE");
HttpPatch request = new HttpPatch(url);
request.addHeader("X-API-Key", apiKey);
request.addHeader("Content-Type", "application/json");
request.addHeader("Accept", "application/json");
request.setEntity(new StringEntity(root.toString()));
try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null;
if (statusCode == HttpStatus.SC_NO_CONTENT) {
logger.debug("Record {} {} deleted", normalizedRecord, type);
return;
}
if (statusCode == HttpStatus.SC_NOT_FOUND) {
logger.debug("Record {} {} not found (idempotent delete)", normalizedRecord, type);
return;
}
if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) {
throw new CloudRuntimeException("Invalid PowerDNS API key");
}
logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body);
throw new CloudRuntimeException("Failed to delete record " + normalizedRecord);
}
} catch (IOException e) {
throw new CloudRuntimeException("Error while deleting PowerDNS record", e);
}
}
public Iterable<JsonNode> listRecords(String baseUrl, String apiKey, String zoneName) {
String normalizedZone = formatZoneName(zoneName);
try {
String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
String url = buildApiUrl(baseUrl, "/servers/localhost/zones/" + encodedZone);
HttpGet request = new HttpGet(url);
request.addHeader("X-API-Key", apiKey);
request.addHeader("Accept", "application/json");
try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null;
if (statusCode == HttpStatus.SC_OK) {
JsonNode zone = MAPPER.readTree(body);
JsonNode rrsets = zone.path("rrsets");
if (rrsets.isArray()) {
return rrsets;
}
return Collections.emptyList();
}
if (statusCode == HttpStatus.SC_NOT_FOUND) {
throw new CloudRuntimeException("Zone not found: " + normalizedZone);
}
if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) {
throw new CloudRuntimeException("Invalid PowerDNS API key");
}
throw new CloudRuntimeException("Failed to list records for zone " + normalizedZone);
}
} catch (IOException e) {
throw new CloudRuntimeException("Error while listing PowerDNS records", e);
}
}
public PowerDnsClient() {
RequestConfig config = RequestConfig.custom()
@ -204,6 +359,42 @@ public class PowerDnsClient implements AutoCloseable {
return normalizedUrl;
}
private String formatZoneName(String zoneName) {
String zone = zoneName.trim().toLowerCase();
if (!zone.endsWith(".")) {
zone += ".";
}
return zone;
}
private String formatRecordName(String recordName, String zoneName) {
if (recordName == null) {
throw new IllegalArgumentException("Record name cannot be null");
}
String normalizedZone = formatZoneName(zoneName);
String zoneWithoutDot = normalizedZone.substring(0, normalizedZone.length() - 1);
String name = recordName.trim().toLowerCase();
// Root record
if (name.equals("@") || name.isEmpty()) {
return normalizedZone;
}
// Already absolute
if (name.endsWith(".")) {
return name;
}
// Fully qualified but missing trailing dot
if (name.equals(zoneWithoutDot) || name.endsWith("." + zoneWithoutDot)) {
return name + ".";
}
// Relative name
return name + "." + normalizedZone;
}
private String buildApiUrl(String baseUrl, String path) {
return normalizeBaseUrl(baseUrl) + "/api/v1" + path;
}

View File

@ -17,6 +17,7 @@
package org.apache.cloudstack.dns.powerdns;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -28,6 +29,7 @@ import org.apache.cloudstack.dns.DnsZone;
import com.cloud.utils.StringUtils;
import com.cloud.utils.component.AdapterBase;
import com.fasterxml.jackson.databind.JsonNode;
public class PowerDnsProvider extends AdapterBase implements DnsProvider {
@ -46,7 +48,7 @@ public class PowerDnsProvider extends AdapterBase implements DnsProvider {
@Override
public void provisionZone(DnsServer server, DnsZone zone) {
validateServerZoneParams(server, zone);
client.createZone(server.getUrl(), server.getApiKey(), zone.getName(), null);
client.createZone(server.getUrl(), server.getApiKey(), zone.getName(), server.getNameServers());
}
@Override
@ -55,24 +57,61 @@ public class PowerDnsProvider extends AdapterBase implements DnsProvider {
client.deleteZone(server.getUrl(), server.getApiKey(), zone.getName());
}
@Override
public DnsRecord createRecord(DnsServer server, DnsZone zone, DnsRecord record) {
return null;
public enum ChangeType {
REPLACE, DELETE
}
@Override
public boolean updateRecord(DnsServer server, DnsZone zone, DnsRecord record) {
return false;
public void addRecord(DnsServer server, DnsZone zone, DnsRecord record) {
validateServerZoneParams(server, zone);
applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.REPLACE);
}
@Override
public boolean deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) {
return false;
public void updateRecord(DnsServer server, DnsZone zone, DnsRecord record) {
addRecord(server, zone, record);
}
@Override
public void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) {
validateServerZoneParams(server, zone);
applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.DELETE);
}
public void applyRecord(String serverUrl, String apiKey, String zoneName, DnsRecord record, ChangeType changeType) {
client.modifyRecord(serverUrl, apiKey, zoneName, record.getName(), record.getType().name(),
record.getTtl(), record.getContents(), changeType.name());
}
@Override
public List<DnsRecord> listRecords(DnsServer server, DnsZone zone) {
return List.of();
List<DnsRecord> records = new ArrayList<>();
for (JsonNode rrset: client.listRecords(server.getUrl(), server.getApiKey(), zone.getName())) {
String name = rrset.path("name").asText();
String typeStr = rrset.path("type").asText();
int ttl = rrset.path("ttl").asInt(0);
if (!"SOA".equalsIgnoreCase(typeStr)) {
try {
List<String> contents = new ArrayList<>();
JsonNode recordsNode = rrset.path("records");
if (recordsNode.isArray()) {
for (JsonNode rec : recordsNode) {
String content = rec.path("content").asText();
if (!content.isEmpty()) {
contents.add(content);
}
}
}
records.add(new DnsRecord(name, DnsRecord.RecordType.valueOf(typeStr), contents, ttl));
} catch (Exception ignored) {
// Skip unsupported record types
}
}
}
return records;
}
void validateServerZoneParams(DnsServer server, DnsZone zone) {

View File

@ -181,6 +181,10 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
if (cmd.getNameServers() != null) {
dnsServer.setNameServers(cmd.getNameServers());
}
if (cmd.getState() != null) {
dnsServer.setState(cmd.getState());
}
if (validationRequired) {
DnsProvider provider = getProvider(dnsServer.getProviderType());
try {
@ -190,6 +194,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
throw new InvalidParameterValueException("Validation failed for DNS server");
}
}
boolean updateStatus = dnsServerDao.update(dnsServerId, dnsServer);
if (updateStatus) {
@ -308,17 +313,79 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
@Override
public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) {
return null;
DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId());
if (zone == null) {
throw new InvalidParameterValueException("DNS Zone not found.");
}
Account caller = CallContext.current().getCallingAccount();
accountMgr.checkAccess(caller, null, true, zone);
DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId());
try {
DnsRecord record = new DnsRecord(cmd.getName(), cmd.getType(), cmd.getContents(), cmd.getTtl());
DnsProvider provider = getProvider(server.getProviderType());
// Add Record via Provider
provider.addRecord(server, zone, record);
return createDnsRecordResponse(record);
} catch (Exception ex) {
logger.error("Failed to add DNS record via provider", ex);
throw new CloudRuntimeException(String.format("Failed to add DNS record: %s", cmd.getName()));
}
}
@Override
public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) {
return false;
DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId());
if (zone == null) {
throw new InvalidParameterValueException("DNS Zone not found.");
}
Account caller = CallContext.current().getCallingAccount();
accountMgr.checkAccess(caller, null, true, zone);
DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId());
try {
// Reconstruct the record DTO just for deletion criteria
DnsRecord record = new DnsRecord();
record.setName(cmd.getName());
record.setType(cmd.getType());
DnsProvider provider = getProvider(server.getProviderType());
provider.deleteRecord(server, zone, record);
return true;
} catch (Exception ex) {
logger.error("Failed to delete DNS record via provider", ex);
throw new CloudRuntimeException(String.format("Failed to delete record: %s", cmd.getName()));
}
}
@Override
public ListResponse<DnsRecordResponse> listDnsRecords(ListDnsRecordsCmd cmd) {
return null;
DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId());
if (zone == null) {
throw new InvalidParameterValueException(String.format("DNS Zone with ID %s not found.", cmd.getDnsZoneId()));
}
Account caller = CallContext.current().getCallingAccount();
accountMgr.checkAccess(caller, null, true, zone);
DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId());
if (server == null) {
throw new CloudRuntimeException("The underlying DNS Server for this zone is missing.");
}
try {
DnsProvider provider = getProvider(server.getProviderType());
List<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 (Exception ex) {
logger.error("Failed to list DNS records from provider", ex);
throw new CloudRuntimeException("Failed to fetch DNS records");
}
}
@Override
@ -393,6 +460,15 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
return res;
}
@Override
public DnsRecordResponse createDnsRecordResponse(DnsRecord record) {
DnsRecordResponse res = new DnsRecordResponse();
res.setName(record.getName());
res.setType(record.getType());
res.setContent(record.getContents());
return res;
}
@Override
public boolean start() {
if (dnsProviders == null || dnsProviders.isEmpty()) {

View File

@ -18,6 +18,7 @@
package org.apache.cloudstack.dns.vo;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import javax.persistence.Column;
@ -95,7 +96,7 @@ public class DnsServerVO implements DnsServer {
}
public DnsServerVO(String name, String url, DnsProviderType providerType, String apiKey,
Integer port, boolean isPublic, String publicDomainSuffix, String nameServers,
Integer port, boolean isPublic, String publicDomainSuffix, List<String> nameServers,
long accountId) {
this();
this.name = name;
@ -105,9 +106,9 @@ public class DnsServerVO implements DnsServer {
this.apiKey = apiKey;
this.accountId = accountId;
this.publicDomainSuffix = publicDomainSuffix;
this.nameServers = nameServers;
this.isPublic = isPublic;
this.state = State.Enabled;
this.nameServers = String.join(",", nameServers);;
}
@Override
@ -204,4 +205,8 @@ public class DnsServerVO implements DnsServer {
public void setName(String name) {
this.name = name;
}
public String getNameServers() {
return nameServers;
}
}