mirror of https://github.com/apache/cloudstack.git
following changes are done:
1. refactored client 2. added exceptions 3. enhanced updateZone 4. ownership check for deleteDnsServer
This commit is contained in:
parent
e011ce1186
commit
4a9f66d532
|
|
@ -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 " +
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue