mirror of https://github.com/apache/cloudstack.git
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:
parent
f29b8be24d
commit
9b77c3708e
|
|
@ -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 " +
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue