following changes are done:

1. refactored client
2. added exceptions
3. enhanced updateZone
4. ownership check for deleteDnsServer
This commit is contained in:
Manoj Kumar 2026-02-19 23:48:57 +05:30
parent e011ce1186
commit 4a9f66d532
No known key found for this signature in database
GPG Key ID: E952B7234D2C6F88
17 changed files with 580 additions and 381 deletions

View File

@ -1337,8 +1337,21 @@ public class ApiConstants {
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 DNS_SERVER_ID = "dnsserverid";
public static final String CONTENT = "content";
public static final String CONTENTS = "contents";
public static final String PUBLIC_DOMAIN_SUFFIX = "publicdomainsuffix";
public static final String AUTHORITATIVE = "authoritative";
public static final String KIND = "kind";
public static final String DNS_SEC = "dnssec";
public static final String TTL = "ttl";
public static final String CHANGE_TYPE = "changetype";
public static final String RECORDS = "records";
public static final String RR_SETS = "rrsets";
public static final String X_API_KEY = "X-API-Key";
public static final String DISABLED = "disabled";
public static final String CONTENT_TYPE = "Content-Type";
public static final String PARAMETER_DESCRIPTION_ACTIVATION_RULE = "Quota tariff's activation rule. It can receive a JS script that results in either " +
"a boolean or a numeric value: if it results in a boolean value, the tariff value will be applied according to the result; if it results in a numeric value, the " +

View File

@ -51,7 +51,7 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd {
description = "The name of the DNS zone (e.g. example.com)")
private String name;
@Parameter(name = "dnsserverid", type = CommandType.UUID, entityType = DnsServerResponse.class,
@Parameter(name = ApiConstants.DNS_SERVER_ID, type = CommandType.UUID, entityType = DnsServerResponse.class,
required = true, description = "The ID of the DNS server to host this zone")
private Long dnsServerId;

View File

@ -19,6 +19,8 @@ package org.apache.cloudstack.dns;
import java.util.List;
import org.apache.cloudstack.dns.exception.DnsProviderException;
import com.cloud.utils.component.Adapter;
public interface DnsProvider extends Adapter {
@ -28,11 +30,12 @@ public interface DnsProvider extends Adapter {
void validate(DnsServer server) throws Exception;
// Zone Operations
String provisionZone(DnsServer server, DnsZone zone);
void deleteZone(DnsServer server, DnsZone zone) ;
String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException;
void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException;
void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException;
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);
String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException;
List<DnsRecord> listRecords(DnsServer server, DnsZone zone) throws DnsProviderException;
String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException;
void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException;
}

View File

@ -61,7 +61,6 @@ 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);

View File

@ -18,11 +18,13 @@
package org.apache.cloudstack.dns;
import java.util.Date;
import java.util.List;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
public interface DnsServer extends InternalIdentity, Identity {
public interface DnsServer extends InternalIdentity, Identity, ControlledEntity {
enum State {
Enabled, Disabled
};
@ -33,7 +35,7 @@ public interface DnsServer extends InternalIdentity, Identity {
DnsProviderType getProviderType();
String getNameServers();
List<String> getNameServers();
String getApiKey();

View File

@ -0,0 +1,27 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.dns.exception;
/**
* Thrown when authentication to the DNS provider fails.
*/
public class DnsAuthenticationException extends DnsProviderException {
public DnsAuthenticationException(String message) {
super(message);
}
}

View File

@ -0,0 +1,27 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.dns.exception;
/**
* Thrown when attempting to create a zone or record that already exists.
*/
public class DnsConflictException extends DnsProviderException {
public DnsConflictException(String message) {
super(message);
}
}

View File

@ -0,0 +1,27 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.dns.exception;
/**
* Thrown when the requested zone or record does not exist.
*/
public class DnsNotFoundException extends DnsProviderException {
public DnsNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,27 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.dns.exception;
/**
* Thrown for unexpected or unknown errors returned by the DNS provider.
*/
public class DnsOperationException extends DnsProviderException {
public DnsOperationException(String message) {
super(message);
}
}

View File

@ -0,0 +1,28 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.dns.exception;
public class DnsProviderException extends Exception {
public DnsProviderException(String message) {
super(message);
}
public DnsProviderException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,30 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.dns.exception;
import java.io.IOException;
/**
* Thrown when HTTP or network errors occur communicating with the DNS provider.
*/
public class DnsTransportException extends DnsProviderException {
public DnsTransportException(String message, IOException cause) {
super(message, cause);
}
}

View File

@ -68,6 +68,7 @@ CREATE TABLE `cloud`.`dns_server` (
`is_public` tinyint(1) NOT NULL DEFAULT '0',
`public_domain_suffix` VARCHAR(255),
`state` ENUM('Enabled', 'Disabled') NOT NULL DEFAULT 'Disabled',
`domain_id` bigint unsigned COMMENT 'for domain-specific ownership',
`account_id` bigint(20) unsigned NOT NULL,
`created` datetime NOT NULL COMMENT 'date created',
`removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)',

View File

@ -20,28 +20,34 @@ 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 java.util.concurrent.TimeUnit;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.dns.exception.DnsAuthenticationException;
import org.apache.cloudstack.dns.exception.DnsConflictException;
import org.apache.cloudstack.dns.exception.DnsNotFoundException;
import org.apache.cloudstack.dns.exception.DnsOperationException;
import org.apache.cloudstack.dns.exception.DnsProviderException;
import org.apache.cloudstack.dns.exception.DnsTransportException;
import org.apache.commons.collections.CollectionUtils;
import org.apache.http.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.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.cloud.utils.StringUtils;
import com.cloud.utils.exception.CloudRuntimeException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
@ -50,359 +56,250 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
public class PowerDnsClient implements AutoCloseable {
public static final Logger logger = LoggerFactory.getLogger(PowerDnsClient.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final int TIMEOUT_MS = 5000;
private static final int CONNECT_TIMEOUT_MS = 5_000;
private static final int SOCKET_TIMEOUT_MS = 10_000;
private static final int MAX_CONNECTIONS_TOTAL = 50;
private static final int MAX_CONNECTIONS_PER_ROUTE = 10;
private static final String API_PREFIX = "/api/v1";
private static final String DEFAULT_SERVER = "localhost";
private final CloseableHttpClient httpClient;
public void validate(String baseUrl, String apiKey) {
String checkUrl = buildApiUrl(baseUrl, "/servers");
HttpGet request = new HttpGet(checkUrl);
request.addHeader("X-API-Key", apiKey);
request.addHeader("Content-Type", "application/json");
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 root = MAPPER.readTree(body);
if (!root.isArray() || root.isEmpty()) {
throw new CloudRuntimeException("No servers returned by PowerDNS API");
}
boolean authoritativeFound = false;
for (JsonNode node : root) {
if ("authoritative".equalsIgnoreCase(node.path("daemon_type").asText(null))) {
authoritativeFound = true;
break;
}
}
if (!authoritativeFound) {
throw new CloudRuntimeException("No authoritative PowerDNS server found");
}
} else if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) {
throw new CloudRuntimeException("Invalid PowerDNS API key");
} else {
logger.debug("Unexpected PowerDNS response: HTTP {} Body: {}", statusCode, body);
throw new CloudRuntimeException(String.format("PowerDNS validation failed with HTTP %d", statusCode));
}
} catch (IOException ex) {
throw new CloudRuntimeException("Failed to connect to PowerDNS", ex);
}
}
public String 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();
json.put("name", normalizedZone);
json.put("kind", "Native");
json.put("dnssec", false);
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");
request.addHeader("Accept", "application/json");
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) {
JsonNode root = MAPPER.readTree(body);
String zoneId = root.path("id").asText();
if (StringUtils.isBlank(zoneId)) {
throw new CloudRuntimeException("PowerDNS returned empty zone id");
}
return zoneId;
}
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 void deleteZone(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);
HttpDelete request = new HttpDelete(url);
request.addHeader("X-API-Key", apiKey);
request.addHeader("Content-Type", "application/json");
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_NO_CONTENT) {
logger.debug("Zone {} deleted successfully", normalizedZone);
return;
}
if (statusCode == HttpStatus.SC_NOT_FOUND) {
logger.debug("Zone {} not found in PowerDNS", normalizedZone);
return;
}
if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) {
throw new CloudRuntimeException("Invalid PowerDNS API key");
}
logger.debug("Unexpected PowerDNS response while deleting zone: HTTP {} Body: {}", statusCode, body);
throw new CloudRuntimeException(String.format("Failed to delete zone %s (HTTP %d)", normalizedZone, statusCode));
}
} catch (IOException e) {
throw new CloudRuntimeException("Error while deleting PowerDNS zone " + zoneName, e);
}
}
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()
.setConnectTimeout(TIMEOUT_MS)
.setConnectionRequestTimeout(TIMEOUT_MS)
.setSocketTimeout(TIMEOUT_MS)
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(MAX_CONNECTIONS_TOTAL);
connectionManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT_MS)
.setConnectionRequestTimeout(CONNECT_TIMEOUT_MS)
.setSocketTimeout(SOCKET_TIMEOUT_MS)
.build();
this.httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(config)
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
.evictIdleConnections(30, TimeUnit.SECONDS)
.disableCookieManagement()
.build();
}
private String normalizeBaseUrl(String baseUrl) {
if (baseUrl == null) {
throw new IllegalArgumentException("PowerDNS base URL cannot be null");
public void validate(String baseUrl, String apiKey) throws DnsProviderException {
String url = buildUrl(baseUrl, "/servers");
HttpGet request = new HttpGet(url);
JsonNode servers = execute(request, apiKey, 200);
if (servers == null || !servers.isArray() || servers.isEmpty()) {
throw new DnsOperationException("No servers returned by PowerDNS API");
}
String normalizedUrl = baseUrl.trim();
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
normalizedUrl = "http://" + normalizedUrl;
boolean authoritativeFound = false;
for (JsonNode server : servers) {
if (ApiConstants.AUTHORITATIVE.equalsIgnoreCase(server.path("daemon_type").asText(null))) {
authoritativeFound = true;
break;
}
}
if (normalizedUrl.endsWith("/")) {
normalizedUrl = normalizedUrl.substring(0, normalizedUrl.length() - 1);
if (!authoritativeFound) {
throw new DnsOperationException("No authoritative PowerDNS server found");
}
return normalizedUrl;
}
private String formatZoneName(String zoneName) {
public String createZone(String baseUrl, String apiKey, String zoneName, String zoneKind, boolean dnsSecFlag,
List<String> nameServers) throws DnsProviderException {
validate(baseUrl, apiKey);
String normalizedZone = normalizeZone(zoneName);
ObjectNode json = MAPPER.createObjectNode();
json.put(ApiConstants.NAME, normalizedZone);
json.put(ApiConstants.KIND, zoneKind);
json.put(ApiConstants.DNS_SEC, dnsSecFlag);
if (!CollectionUtils.isEmpty(nameServers)) {
ArrayNode nsArray = json.putArray(ApiConstants.NAME_SERVERS);
for (String ns : nameServers) {
nsArray.add(ns.endsWith(".") ? ns : ns + ".");
}
}
HttpPost request = new HttpPost(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones"));
request.setEntity(new StringEntity(json.toString(), StandardCharsets.UTF_8));
JsonNode response = execute(request, apiKey, 201);
if (response == null) {
throw new DnsOperationException("Empty response from DNS server");
}
String zoneId = response.path(ApiConstants.ID).asText();
if (StringUtils.isBlank(zoneId)) {
throw new DnsOperationException("PowerDNS returned empty zone id");
}
return zoneId;
}
public void updateZone(String baseUrl, String apiKey, String zoneName, String zoneKind, Boolean dnsSecFlag,
List<String> nameServers) throws DnsProviderException {
String normalizedZone = normalizeZone(zoneName);
String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
String url = buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone);
ObjectNode json = MAPPER.createObjectNode();
if (dnsSecFlag != null) {
json.put(ApiConstants.DNS_SEC, dnsSecFlag);
}
if (StringUtils.isNotBlank(zoneKind)) {
json.put(ApiConstants.KIND, zoneKind);
}
if (!CollectionUtils.isEmpty(nameServers)) {
ArrayNode nsArray = json.putArray(ApiConstants.NAME_SERVERS);
for (String ns : nameServers) {
nsArray.add(ns.endsWith(".") ? ns : ns + ".");
}
}
HttpPatch request = new HttpPatch(url);
request.setEntity(new org.apache.http.entity.StringEntity(json.toString(), StandardCharsets.UTF_8));
execute(request, apiKey, 204);
}
public void deleteZone(String baseUrl, String apiKey, String zoneName) throws DnsProviderException {
validate(baseUrl, apiKey);
String normalizedZone = normalizeZone(zoneName);
String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
HttpDelete request = new HttpDelete(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone));
execute(request, apiKey, 204, 404);
}
public String modifyRecord(String baseUrl, String apiKey, String zoneName, String recordName, String type, long ttl,
List<String> contents, String changeType) throws DnsProviderException {
validate(baseUrl, apiKey);
String normalizedZone = normalizeZone(zoneName);
String normalizedRecord = normalizeRecordName(recordName, normalizedZone);
ObjectNode root = MAPPER.createObjectNode();
ArrayNode rrsets = root.putArray(ApiConstants.RR_SETS);
ObjectNode rrset = rrsets.addObject();
rrset.put(ApiConstants.NAME, normalizedRecord);
rrset.put(ApiConstants.TYPE, type.toUpperCase());
rrset.put(ApiConstants.TTL, ttl);
rrset.put(ApiConstants.CHANGE_TYPE, changeType);
ArrayNode records = rrset.putArray(ApiConstants.RECORDS);
if (!CollectionUtils.isEmpty(contents)) {
for (String content : contents) {
ObjectNode record = records.addObject();
record.put(ApiConstants.CONTENT, content);
record.put(ApiConstants.DISABLED, false);
}
}
String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
HttpPatch request = new HttpPatch(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone));
request.setEntity(new org.apache.http.entity.StringEntity(root.toString(), StandardCharsets.UTF_8));
execute(request, apiKey, 204);
return normalizedRecord;
}
public Iterable<JsonNode> listRecords(String baseUrl, String apiKey, String zoneName) throws DnsProviderException {
validate(baseUrl, apiKey);
String normalizedZone = normalizeZone(zoneName);
String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8);
HttpGet request = new HttpGet(buildUrl(baseUrl, "/servers/" + DEFAULT_SERVER + "/zones/" + encodedZone));
JsonNode zoneNode = execute(request, apiKey, 200);
if (zoneNode == null || !zoneNode.has(ApiConstants.RR_SETS)) {
return Collections.emptyList();
}
JsonNode rrsets = zoneNode.path(ApiConstants.RR_SETS);
return rrsets.isArray() ? rrsets : Collections.emptyList();
}
private JsonNode execute(HttpUriRequest request, String apiKey, int... expectedStatus) throws DnsProviderException {
request.addHeader(ApiConstants.X_API_KEY, apiKey);
request.addHeader("Accept", "application/json");
request.addHeader(ApiConstants.CONTENT_TYPE, "application/json");
try (CloseableHttpResponse response = httpClient.execute(request)) {
int status = response.getStatusLine().getStatusCode();
String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : null;
for (int expected : expectedStatus) {
if (status == expected) {
if (body != null && !body.isEmpty()) {
return MAPPER.readTree(body);
} else {
return null;
}
}
}
if (status == 404) {
throw new DnsNotFoundException("Resource not found: " + body);
} else if (status == 401 || status == 403) {
throw new DnsAuthenticationException("Invalid API key");
} else if (status == 409) {
throw new DnsConflictException("Conflict: " + body);
}
throw new DnsOperationException("Unexpected PowerDNS response: HTTP " + status + " Body: " + body);
} catch (IOException ex) {
throw new DnsTransportException("Error communicating with PowerDNS", ex);
}
}
private String buildUrl(String baseUrl, String path) {
return normalizeBaseUrl(baseUrl) + API_PREFIX + path;
}
private String normalizeBaseUrl(String baseUrl) {
if (baseUrl == null) {
throw new IllegalArgumentException("Base URL cannot be null");
}
String url = baseUrl.trim();
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "http://" + url;
}
if (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
return url;
}
private String normalizeZone(String zoneName) {
if (StringUtils.isBlank(zoneName)) {
throw new IllegalArgumentException("Zone name must not be null or empty");
}
String zone = zoneName.trim().toLowerCase();
if (!zone.endsWith(".")) {
zone += ".";
zone = zone + ".";
}
if (zone.length() < 2) {
throw new IllegalArgumentException("Zone name is too short");
}
return zone;
}
private String formatRecordName(String recordName, String zoneName) {
String normalizeRecordName(String recordName, String zoneName) {
if (recordName == null) {
throw new IllegalArgumentException("Record name cannot be null");
throw new IllegalArgumentException("Record name must not be null");
}
String normalizedZone = formatZoneName(zoneName);
String zoneWithoutDot = normalizedZone.substring(0, normalizedZone.length() - 1);
String normalizedZone = normalizeZone(zoneName);
String name = recordName.trim().toLowerCase();
// Root record
// Apex of the zone
if (name.equals("@") || name.isEmpty()) {
return normalizedZone;
}
// Already absolute
String zoneWithoutDot = normalizedZone.substring(0, normalizedZone.length() - 1);
// Already absolute (ends with dot)
if (name.endsWith(".")) {
// Check if the record belongs to the zone
if (!name.equals(normalizedZone) && !name.endsWith("." + zoneWithoutDot + ".")) {
throw new IllegalArgumentException(
String.format("Record '%s' does not belong to zone '%s'", recordName, zoneName)
);
}
return name;
}
// Fully qualified but missing trailing dot
if (name.equals(zoneWithoutDot) || name.endsWith("." + zoneWithoutDot)) {
if (name.contains(".")) {
return name + ".";
}
// Relative name
// Relative name append zone
return name + "." + normalizedZone;
}
private String buildApiUrl(String baseUrl, String path) {
return normalizeBaseUrl(baseUrl) + "/api/v1" + path;
}
@Override
public void close() {
try {

View File

@ -26,6 +26,7 @@ import org.apache.cloudstack.dns.DnsProviderType;
import org.apache.cloudstack.dns.DnsRecord;
import org.apache.cloudstack.dns.DnsServer;
import org.apache.cloudstack.dns.DnsZone;
import org.apache.cloudstack.dns.exception.DnsProviderException;
import com.cloud.utils.StringUtils;
import com.cloud.utils.component.AdapterBase;
@ -40,54 +41,65 @@ public class PowerDnsProvider extends AdapterBase implements DnsProvider {
return DnsProviderType.PowerDNS;
}
public void validate(DnsServer server) {
public void validate(DnsServer server) throws DnsProviderException {
validateServerParams(server);
client.validate(server.getUrl(), server.getApiKey());
}
@Override
public String provisionZone(DnsServer server, DnsZone zone) {
public String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException {
validateServerZoneParams(server, zone);
return client.createZone(server.getUrl(), server.getApiKey(), zone.getName(), server.getNameServers());
return client.createZone(server.getUrl(),
server.getApiKey(),
zone.getName(),
"Native",
false,
server.getNameServers()
);
}
@Override
public void deleteZone(DnsServer server, DnsZone zone) {
public void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException {
validateServerZoneParams(server, zone);
client.deleteZone(server.getUrl(), server.getApiKey(), zone.getName());
}
@Override
public void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException {
validateServerZoneParams(server, zone);
client.updateZone(server.getUrl(), server.getApiKey(), zone.getName(), "Native", false, server.getNameServers());
}
public enum ChangeType {
REPLACE, DELETE
}
@Override
public void addRecord(DnsServer server, DnsZone zone, DnsRecord record) {
public String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException {
validateServerZoneParams(server, zone);
applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.REPLACE);
return applyRecord(server.getUrl(), server.getApiKey(), zone.getName(), record, ChangeType.REPLACE);
}
@Override
public void updateRecord(DnsServer server, DnsZone zone, DnsRecord record) {
addRecord(server, zone, record);
public String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException {
validateServerZoneParams(server, zone);
return addRecord(server, zone, record);
}
@Override
public void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) {
public void deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException {
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(),
public String applyRecord(String serverUrl, String apiKey, String zoneName, DnsRecord record, ChangeType changeType) throws DnsProviderException {
return 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) {
public List<DnsRecord> listRecords(DnsServer server, DnsZone zone) throws DnsProviderException {
validateServerZoneParams(server, zone);
List<DnsRecord> records = new ArrayList<>();
for (JsonNode rrset: client.listRecords(server.getUrl(), server.getApiKey(), zone.getName())) {
String name = rrset.path("name").asText();
@ -114,7 +126,7 @@ public class PowerDnsProvider extends AdapterBase implements DnsProvider {
return records;
}
void validateServerZoneParams(DnsServer server, DnsZone zone) {
void validateServerZoneParams(DnsServer server, DnsZone zone) throws DnsProviderException {
validateServerParams(server);
if (StringUtils.isBlank(zone.getName())) {
throw new IllegalArgumentException("Zone name cannot be empty");
@ -130,8 +142,6 @@ public class PowerDnsProvider extends AdapterBase implements DnsProvider {
}
}
@Override
public boolean configure(String name, Map<String, Object> params) {
if (client == null) {

View File

@ -0,0 +1,84 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.dns.powerdns;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.mockito.InjectMocks;
public class PowerDnsClientTest {
@InjectMocks
PowerDnsClient client = new PowerDnsClient();
@Test
public void testNormalizeApexRecord() {
String result = client.normalizeRecordName("@", "example.com");
assertEquals("example.com.", result);
result = client.normalizeRecordName("", "example.com");
assertEquals("example.com.", result);
}
@Test
public void testNormalizeRelativeRecord() {
String result = client.normalizeRecordName("www", "example.com");
assertEquals("www.example.com.", result);
result = client.normalizeRecordName("WWW", "example.com"); // test case-insensitive
assertEquals("www.example.com.", result);
}
@Test
public void testNormalizeAbsoluteRecordWithinZone() {
String result = client.normalizeRecordName("www.example.com.", "example.com");
assertEquals("www.example.com.", result);
}
@Test(expected = IllegalArgumentException.class)
public void testNormalizeAbsoluteRecordOutsideZoneThrows() {
client.normalizeRecordName("other.com.", "example.com");
}
@Test
public void testNormalizeDottedNameWithoutTrailingDot() {
String result = client.normalizeRecordName("api.test.com", "example.com");
assertEquals("api.test.com.", result);
}
@Test
public void testNormalizeRelativeSubdomain() {
String result = client.normalizeRecordName("mail", "example.com");
assertEquals("mail.example.com.", result);
}
@Test(expected = IllegalArgumentException.class)
public void testNormalizeNullRecordNameThrows() {
client.normalizeRecordName(null, "example.com");
}
@Test
public void testNormalizeZoneNormalization() {
String result = client.normalizeRecordName("www", "Example.Com");
assertEquals("www.example.com.", result);
result = client.normalizeRecordName("www", "example.com.");
assertEquals("www.example.com.", result);
}
}

View File

@ -61,6 +61,7 @@ import com.cloud.network.dao.NetworkVO;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.utils.Pair;
import com.cloud.utils.StringUtils;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.component.PluggableService;
import com.cloud.utils.db.Filter;
@ -161,9 +162,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
}
Account caller = CallContext.current().getCallingAccount();
if (!accountMgr.isRootAdmin(caller.getId()) && dnsServer.getAccountId() != caller.getId()) {
throw new PermissionDeniedException("You do not have permission to update this DNS server.");
}
accountMgr.checkAccess(caller, null, true, dnsServer);
boolean validationRequired = false;
String originalUrl = dnsServer.getUrl();
@ -234,9 +233,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
throw new InvalidParameterValueException(String.format("DNS server with ID: %s not found.", dnsServerId));
}
Account caller = CallContext.current().getCallingAccount();
if (!accountMgr.isRootAdmin(caller.getId()) && dnsServer.getAccountId() != caller.getId()) {
throw new PermissionDeniedException("You do not have permission to delete this DNS server.");
}
accountMgr.checkAccess(caller, null, true, dnsServer);
return dnsServerDao.remove(dnsServerId);
}
@ -248,6 +245,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
response.setUrl(server.getUrl());
response.setProvider(server.getProviderType());
response.setPublic(server.isPublic());
response.setNameServers(server.getNameServers());
response.setObjectName("dnsserver");
return response;
}
@ -270,8 +268,8 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
if (server != null && zone.getState() == DnsZone.State.Active) {
try {
DnsProvider provider = getProvider(server.getProviderType());
logger.debug("Deleting DNS zone: {} from provider.", zone.getName());
provider.deleteZone(server, zone);
logger.debug("Deleted DNS zone: {}", zone.getName());
} catch (Exception ex) {
logger.error("Failed to delete DNS zone from provider", ex);
throw new CloudRuntimeException("Failed to delete DNS zone.");
@ -287,26 +285,32 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
@Override
public DnsZone updateDnsZone(UpdateDnsZoneCmd cmd) {
DnsZoneVO zone = dnsZoneDao.findById(cmd.getId());
if (zone == null) {
DnsZoneVO dnsZone = dnsZoneDao.findById(cmd.getId());
if (dnsZone == null) {
throw new InvalidParameterValueException("DNS zone not found.");
}
// ACL Check
Account caller = CallContext.current().getCallingAccount();
accountMgr.checkAccess(caller, null, true, zone);
// Update fields
accountMgr.checkAccess(caller, null, true, dnsZone);
boolean updated = false;
if (cmd.getDescription() != null) {
zone.setDescription(cmd.getDescription());
dnsZone.setDescription(cmd.getDescription());
updated = true;
}
if (updated) {
dnsZoneDao.update(zone.getId(), zone);
DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId());
if (server == null) {
throw new CloudRuntimeException("The underlying DNS server for this DNS zone is missing.");
}
try {
DnsProvider provider = getProvider(server.getProviderType());
provider.updateZone(server, dnsZone);
} catch (Exception ex) {
logger.error("Failed to update DNS zone: {} on DNS server: {}", dnsZone.getName(), server.getName(), ex);
throw new CloudRuntimeException("Failed to update DNS zone: " + dnsZone.getName());
}
}
return zone;
return dnsZone;
}
@Override
@ -328,11 +332,6 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
return response;
}
@Override
public DnsZone getDnsZone(long id) {
return null;
}
@Override
public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) {
DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId());
@ -344,10 +343,10 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
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);
DnsRecord record = new DnsRecord(cmd.getName(), cmd.getType(), cmd.getContents(), cmd.getTtl());
String normalizedRecordName = provider.addRecord(server, zone, record);
record.setName(normalizedRecordName);
return createDnsRecordResponse(record);
} catch (Exception ex) {
logger.error("Failed to add DNS record via provider", ex);
@ -449,8 +448,8 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
}
@Override
public DnsZone provisionDnsZone(long zoneId) {
DnsZoneVO dnsZone = dnsZoneDao.findById(zoneId);
public DnsZone provisionDnsZone(long dnsZoneId) {
DnsZoneVO dnsZone = dnsZoneDao.findById(dnsZoneId);
if (dnsZone == null) {
throw new CloudRuntimeException("DNS zone not found during provisioning");
}
@ -460,11 +459,11 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
String externalReferenceId = provider.provisionZone(server, dnsZone);
dnsZone.setExternalReference(externalReferenceId);
dnsZone.setState(DnsZone.State.Active);
logger.debug("DNS zone: {} created successfully on DNS server: {} with ID: {}", dnsZone.getName(), server.getName(), zoneId);
dnsZoneDao.update(dnsZone.getId(), dnsZone);
logger.debug("DNS zone: {} created successfully on DNS server: {} with ID: {}", dnsZone.getName(), server.getName(), dnsZoneId);
} catch (Exception ex) {
dnsZoneDao.remove(dnsZoneId);
logger.error("Failed to provision DNS zone: {} on DNS server: {}", dnsZone.getName(), server.getName(), ex);
dnsZoneDao.remove(zoneId);
throw new CloudRuntimeException("Failed to provision DNS zone: " + dnsZone.getName());
}
return dnsZone;
@ -570,6 +569,10 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
networkId = nic != null ? nic.getNetworkId() : null;
}
// networkId may not be of Shared network type
// there might be multiple shared networks
// possible to have dns record for secondary ip
if (nic == null) {
throw new CloudRuntimeException("No valid NIC found for this Instance on the specified Network.");
}
@ -592,7 +595,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
// Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain")
String recordName = String.valueOf(instance.getInstanceName());
if (map.getSubDomain() != null && !map.getSubDomain().isEmpty()) {
if (StringUtils.isNotBlank(map.getSubDomain() )) {
recordName = recordName + "." + map.getSubDomain();
}
@ -664,6 +667,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
cmdList.add(DeleteDnsZoneCmd.class);
cmdList.add(UpdateDnsZoneCmd.class);
cmdList.add(AssociateDnsZoneToNetworkCmd.class);
cmdList.add(DisassociateDnsZoneFromNetworkCmd.class);
// DNS Record Commands
cmdList.add(CreateDnsRecordCmd.class);

View File

@ -17,6 +17,8 @@
package org.apache.cloudstack.dns.vo;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@ -34,7 +36,9 @@ import javax.persistence.TemporalType;
import org.apache.cloudstack.dns.DnsProviderType;
import org.apache.cloudstack.dns.DnsServer;
import org.apache.cloudstack.dns.DnsZone;
import com.cloud.utils.StringUtils;
import com.cloud.utils.db.Encrypt;
import com.cloud.utils.db.GenericDao;
@ -79,6 +83,9 @@ public class DnsServerVO implements DnsServer {
@Column(name = "account_id")
private long accountId;
@Column(name = "domain_id")
private long domainId;
@Column(name = "name_servers")
private String nameServers;
@ -116,6 +123,11 @@ public class DnsServerVO implements DnsServer {
return id;
}
@Override
public Class<?> getEntityType() {
return DnsZone.class;
}
@Override
public String getName() {
return name;
@ -206,7 +218,15 @@ public class DnsServerVO implements DnsServer {
this.name = name;
}
public String getNameServers() {
return nameServers;
public List<String> getNameServers() {
if (StringUtils.isBlank(nameServers)) {
return Collections.emptyList();
}
return Arrays.asList(nameServers.split(","));
}
@Override
public long getDomainId() {
return domainId;
}
}