From 9911c280e177398455dce3bc49922d7663cc7159 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Thu, 12 Feb 2026 19:27:32 +0530 Subject: [PATCH] Changes done to AddDnsServer, ListDnsServer, DeleteDnsServer and UpdateDnsServer --- .../apache/cloudstack/api/ApiConstants.java | 5 + .../api/command/user/dns/AddDnsServerCmd.java | 64 +++++-- .../command/user/dns/DeleteDnsServerCmd.java | 2 +- .../command/user/dns/ListDnsServersCmd.java | 1 + .../command/user/dns/UpdateDnsServerCmd.java | 112 +++++++++++++ .../api/response/DnsServerResponse.java | 71 +++++--- .../apache/cloudstack/dns/DnsProvider.java | 2 +- .../cloudstack/dns/DnsProviderManager.java | 2 + .../org/apache/cloudstack/dns/DnsServer.java | 2 +- .../META-INF/db/schema-42210to42300.sql | 3 + .../dns/powerdns/PowerDnsClient.java | 118 +++++++++++++ .../dns/powerdns/PowerDnsProvider.java | 32 +++- .../dns/DnsProviderManagerImpl.java | 156 +++++++++++++++++- .../cloudstack/dns/dao/DnsServerDao.java | 8 +- .../cloudstack/dns/dao/DnsServerDaoImpl.java | 53 +++++- .../apache/cloudstack/dns/vo/DnsServerVO.java | 56 ++++++- 16 files changed, 637 insertions(+), 50 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java create mode 100644 plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 1ab6fba6081..a64e40fa7a2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1333,6 +1333,11 @@ public class ApiConstants { public static final String OBJECT_STORAGE_LIMIT = "objectstoragelimit"; public static final String OBJECT_STORAGE_TOTAL = "objectstoragetotal"; + // DNS provider related + public static final String NAME_SERVERS = "nameservers"; + public static final String CREDENTIALS = "credentials"; + public static final String PUBLIC_DOMAIN_SUFFIX = "publicdomainsuffix"; + public static final String PARAMETER_DESCRIPTION_ACTIVATION_RULE = "Quota tariff's activation rule. It can receive a JS script that results in either " + "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 " + "numeric value will be applied; if the result is neither a boolean nor a numeric value, the tariff will not be applied. If the rule is not informed, the tariff " + diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java index aa1218dd357..764992d6e81 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -21,12 +21,15 @@ import javax.inject.Inject; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.dns.DnsServer; +import org.apache.commons.lang3.BooleanUtils; @APICommand(name = "addDnsServer", description = "Adds a new external DNS server", responseObject = DnsServerResponse.class, requestHasSensitiveInfo = true) @@ -45,14 +48,23 @@ public class AddDnsServerCmd extends BaseCmd { @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "API URL of the provider") private String url; - @Parameter(name = "provider", type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") private String provider; - @Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING, description = "API Username") - private String username; + @Parameter(name = ApiConstants.CREDENTIALS, type = CommandType.STRING, required = false, description = "API Key or Credentials for the external provider") + private String credentials; - @Parameter(name = ApiConstants.PASSWORD, type = CommandType.STRING, description = "API Password or Token") - private String password; + @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") + private Integer port; + + @Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, description = "Whether the DNS server is publicly accessible by other accounts") + private Boolean isPublic; + + @Parameter(name = ApiConstants.PUBLIC_DOMAIN_SUFFIX, type = CommandType.STRING, description = "The domain suffix used for public access (e.g. public.example.com)") + private String publicDomainSuffix; + + @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.STRING, description = "Comma separated list of name servers") + private String nameServers; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -61,19 +73,45 @@ public class AddDnsServerCmd extends BaseCmd { public String getName() { return name; } public String getUrl() { return url; } public String getProvider() { return provider; } - public String getUsername() { return username; } - public String getPassword() { return password; } + public String getCredentials() { + return credentials; + } - @Override - public void execute() { - DnsServer server = dnsProviderManager.addDnsServer(this); - DnsServerResponse response = dnsProviderManager.createDnsServerResponse(server); - response.setResponseName(getCommandName()); - setResponseObject(response); + public Integer getPort() { + return port; + } + + public Boolean isPublic() { + return BooleanUtils.isTrue(isPublic); + } + + public String getPublicDomainSuffix() { + return publicDomainSuffix; + } + + public String getNameServers() { + return nameServers; } @Override public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + + @Override + public void execute() { + try { + DnsServer server = dnsProviderManager.addDnsServer(this); + if (server != null) { + DnsServerResponse response = dnsProviderManager.createDnsServerResponse(server); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add DNS server"); + } + } catch (Exception ex) { + logger.error("Failed to add DNS server", ex); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java index 6328c7397f8..155aa200631 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java @@ -76,5 +76,5 @@ public class DeleteDnsServerCmd extends BaseAsyncCmd { public String getEventType() { return EventTypes.EVENT_DNS_SERVER_DELETE; } @Override - public String getEventDescription() { return "Deleting DNS Server ID: " + getId(); } + public String getEventDescription() { return "Deleting DNS server ID: " + getId(); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java index 5a2a4a8778f..cd408dcb6ad 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java @@ -51,6 +51,7 @@ public class ListDnsServersCmd extends BaseListAccountResourcesCmd { public void execute() { ListResponse response = dnsProviderManager.listDnsServers(this); response.setResponseName(getCommandName()); + response.setObjectName("dnsserver"); setResponseObject(response); } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java new file mode 100644 index 00000000000..c9f63b7f06a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java @@ -0,0 +1,112 @@ +// 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.api.command.user.dns; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderManager; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.commons.lang3.BooleanUtils; + +@APICommand(name = "updateDnsServer", description = "Update DNS server", + responseObject = DnsServerResponse.class, requestHasSensitiveInfo = true) +public class UpdateDnsServerCmd extends BaseCmd { + + @Inject + DnsProviderManager dnsProviderManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, + required = true, description = "The ID of the DNS server to update") + private Long id; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Name of the DNS server") + private String name; + + @Parameter(name = ApiConstants.URL, type = CommandType.STRING, description = "API URL of the provider") + private String url; + + @Parameter(name = ApiConstants.CREDENTIALS, type = CommandType.STRING, required = false, description = "API Key or Credentials for the external provider") + private String credentials; + + @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") + private Integer port; + + @Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, description = "Whether the DNS server is publicly accessible by other accounts") + private Boolean isPublic; + + @Parameter(name = ApiConstants.PUBLIC_DOMAIN_SUFFIX, type = CommandType.STRING, description = "The domain suffix used for public access (e.g. public.example.com)") + private String publicDomainSuffix; + + @Parameter(name = ApiConstants.NAME_SERVERS, type = CommandType.STRING, description = "Comma separated list of name servers") + private String nameServers; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { return id; } + public String getName() { return name; } + public String getUrl() { return url; } + public String getCredentials() { + return credentials; + } + public Integer getPort() { + return port; + } + public Boolean isPublic() { + return BooleanUtils.isTrue(isPublic); + } + public String getPublicDomainSuffix() { + return publicDomainSuffix; + } + public String getNameServers() { return nameServers; } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public void execute() { + try { + DnsServer server = dnsProviderManager.updateDnsServer(this); + if (server != null) { + DnsServerResponse response = dnsProviderManager.createDnsServerResponse(server); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update DNS server"); + } + } catch (Exception ex) { + logger.error("Failed to add update server", ex); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage()); + } + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java index 393542cb257..57f6ce28d8f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java @@ -17,9 +17,12 @@ package org.apache.cloudstack.api.response; +import java.util.List; + import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.dns.DnsProviderType; import org.apache.cloudstack.dns.DnsServer; import com.cloud.serializer.Param; @@ -29,39 +32,69 @@ import com.google.gson.annotations.SerializedName; public class DnsServerResponse extends BaseResponse { @SerializedName(ApiConstants.ID) - @Param(description = "the ID of the DNS server") + @Param(description = "ID of the DNS server") private String id; @SerializedName(ApiConstants.NAME) - @Param(description = "the name of the DNS server") + @Param(description = "Name of the DNS server") private String name; @SerializedName(ApiConstants.URL) - @Param(description = "the URL of the DNS server API") + @Param(description = "URL of the DNS server API") private String url; + @SerializedName(ApiConstants.PORT) + @Param(description = "The port of the DNS server") + private Integer port; + @SerializedName(ApiConstants.PROVIDER) - @Param(description = "the provider type of the DNS server") - private String provider; + @Param(description = "The provider type of the DNS server") + private DnsProviderType provider; - @SerializedName(ApiConstants.ACCOUNT) - @Param(description = "the account associated with the DNS server") - private String accountName; + @SerializedName(ApiConstants.IS_PUBLIC) + @Param(description = "Is the DNS server publicly available") + private Boolean isPublic; - @SerializedName(ApiConstants.PROJECT) - @Param(description = "the project name of the DNS server") - private String projectName; + @SerializedName(ApiConstants.PUBLIC_DOMAIN_SUFFIX) @Param(description = "The public domain suffix for the DNS server") + private String publicDomainSuffix; - @SerializedName(ApiConstants.DOMAIN_ID) - @Param(description = "the domain ID of the DNS server") - private String domainId; - - @SerializedName(ApiConstants.DOMAIN) - @Param(description = "the domain name of the DNS server") - private String domainName; + @SerializedName(ApiConstants.NAME_SERVERS) @Param(description = "Name servers entries associated to DNS server") + private List nameServers; public DnsServerResponse() { super(); - setObjectName("dnsserver"); + + } + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setProvider(DnsProviderType provider) { + this.provider = provider; + } + + public void setPublic(Boolean value) { + isPublic = value; + } + + public void setPort(Integer port) { + this.port = port; + } + + public void setPublicDomainSuffix(String publicDomainSuffix) { + this.publicDomainSuffix = publicDomainSuffix; + } + + public void setNameServers(List nameServers) { + this.nameServers = nameServers; } } diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java index 38c155fc72a..c0687571375 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -25,7 +25,7 @@ public interface DnsProvider extends Adapter { DnsProviderType getProviderType(); // Validates connectivity to the server - boolean validate(DnsServer server); + boolean validate(DnsServer server) throws Exception; // Zone Operations boolean createZone(DnsServer server, DnsZone zone); diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java index 0703c559f61..138b32943dc 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -28,6 +28,7 @@ import org.apache.cloudstack.api.command.user.dns.DeleteDnsZoneCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; +import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; @@ -40,6 +41,7 @@ public interface DnsProviderManager extends Manager, PluggableService { DnsServer addDnsServer(AddDnsServerCmd cmd); ListResponse listDnsServers(ListDnsServersCmd cmd); + DnsServer updateDnsServer(UpdateDnsServerCmd cmd); boolean deleteDnsServer(DeleteDnsServerCmd cmd); DnsServerResponse createDnsServerResponse(DnsServer server); diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java index b399a974701..dafd40017e5 100644 --- a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java @@ -33,7 +33,7 @@ public interface DnsServer extends InternalIdentity, Identity { DnsProviderType getProviderType(); - String getAPIKey(); + String getApiKey(); long getAccountId(); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index f549ec44145..d58dc3a56bb 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -60,8 +60,11 @@ CREATE TABLE `cloud`.`dns_server` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns server', `uuid` varchar(255) COMMENT 'uuid of the dns server', `name` varchar(255) NOT NULL COMMENT 'display name of the dns server', + `provider_type` varchar(255) NOT NULL COMMENT 'Provider type such as PowerDns', `url` varchar(1024) NOT NULL COMMENT 'dns server url', + `api_key` varchar(255) NOT NULL COMMENT 'dns server api_key', `port` int(11) DEFAULT NULL COMMENT 'optional dns server port', + `name_servers` varchar(1024) DEFAULT NULL COMMENT 'Comma separated list of name servers', `is_public` tinyint(1) NOT NULL DEFAULT '0', `public_domain_suffix` VARCHAR(255), `state` ENUM('Enabled', 'Disabled') NOT NULL DEFAULT 'Disabled', diff --git a/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java new file mode 100644 index 00000000000..1377519202e --- /dev/null +++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java @@ -0,0 +1,118 @@ +// 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 java.io.IOException; + +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.cloud.utils.exception.CloudRuntimeException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +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 final CloseableHttpClient httpClient; + + public void validate(String baseUrl, String apiKey) { + String normalizedUrl = baseUrl.trim(); + + if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) { + normalizedUrl = "http://" + normalizedUrl; // default to HTTP + } + + if (normalizedUrl.endsWith("/")) { + normalizedUrl = normalizedUrl.substring(0, normalizedUrl.length() - 1); + } + + + String checkUrl = normalizedUrl + "/api/v1/servers"; + + HttpGet request = new HttpGet(checkUrl); + 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 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 PowerDnsClient() { + RequestConfig config = RequestConfig.custom() + .setConnectTimeout(TIMEOUT_MS) + .setConnectionRequestTimeout(TIMEOUT_MS) + .setSocketTimeout(TIMEOUT_MS) + .build(); + + this.httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(config) + .disableCookieManagement() + .build(); + } + + @Override + public void close() { + try { + httpClient.close(); + } catch (IOException e) { + logger.warn("Failed to close PowerDNS HTTP client", e); + } + } +} diff --git a/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java index c18820e4134..ba774745d05 100644 --- a/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java +++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java @@ -18,6 +18,7 @@ package org.apache.cloudstack.dns.powerdns; import java.util.List; +import java.util.Map; import org.apache.cloudstack.dns.DnsProvider; import org.apache.cloudstack.dns.DnsProviderType; @@ -25,17 +26,28 @@ import org.apache.cloudstack.dns.DnsRecord; import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.dns.DnsZone; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.AdapterBase; public class PowerDnsProvider extends AdapterBase implements DnsProvider { + + private PowerDnsClient client; + @Override public DnsProviderType getProviderType() { return DnsProviderType.PowerDNS; } - @Override public boolean validate(DnsServer server) { - return false; + if (StringUtils.isBlank(server.getUrl())) { + throw new IllegalArgumentException("PowerDNS API URL cannot be empty"); + } + if (StringUtils.isBlank(server.getApiKey())) { + throw new IllegalArgumentException("PowerDNS API key cannot be empty"); + } + client.validate(server.getUrl(), server.getApiKey()); + logger.debug("PowerDNS credentials validated for {}", server.getUrl()); + return true; } @Override @@ -67,4 +79,20 @@ public class PowerDnsProvider extends AdapterBase implements DnsProvider { public List listRecords(DnsServer server, DnsZone zone) { return List.of(); } + + @Override + public boolean configure(String name, Map params) { + if (client == null) { + client = new PowerDnsClient(); + } + return true; + } + + @Override + public boolean stop() { + if (client != null) { + client.close(); + } + return true; + } } diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java index 93d3c42d0ac..4d5c0e5d1ca 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -20,6 +20,8 @@ package org.apache.cloudstack.dns; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; import org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd; import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; @@ -30,19 +32,33 @@ import org.apache.cloudstack.api.command.user.dns.ListDnsProvidersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd; import org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd; +import org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd; import org.apache.cloudstack.api.response.DnsRecordResponse; import org.apache.cloudstack.api.response.DnsServerResponse; import org.apache.cloudstack.api.response.DnsZoneResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.dao.DnsServerDao; +import org.apache.cloudstack.dns.vo.DnsServerVO; import org.springframework.stereotype.Component; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; +import com.cloud.utils.db.Filter; import com.cloud.utils.exception.CloudRuntimeException; @Component public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderManager, PluggableService { List dnsProviders; + @Inject + AccountManager accountMgr; + @Inject + DnsServerDao dnsServerDao; private DnsProvider getProvider(DnsProviderType type) { if (type == null) { @@ -58,22 +74,150 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa @Override public DnsServer addDnsServer(AddDnsServerCmd cmd) { - return null; + Account caller = CallContext.current().getCallingAccount(); + DnsServer existing = dnsServerDao.findByUrlAndAccount(cmd.getUrl(), caller.getId()); + if (existing != null) { + throw new InvalidParameterValueException( + "This Account already has a DNS Server integration for URL: " + cmd.getUrl()); + } + + DnsProviderType type = DnsProviderType.fromString(cmd.getProvider()); + DnsProvider provider = getProvider(type); + DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), type, cmd.getCredentials(), cmd.getPort(), + cmd.isPublic(), cmd.getPublicDomainSuffix(), cmd.getNameServers(), caller.getId()); + try { + provider.validate(server); + } catch (Exception ex) { + logger.error("Failed to validate DNS server", ex); + throw new CloudRuntimeException("Failed to validate DNS server"); + } + return dnsServerDao.persist(server); } @Override public ListResponse listDnsServers(ListDnsServersCmd cmd) { - return null; + Account caller = CallContext.current().getCallingAccount(); + Long accountIdFilter = null; + if (accountMgr.isRootAdmin(caller.getId())) { + // Root Admin: Can see all, unless they specifically ask for an account + if (cmd.getAccountName() != null) { + Account target = accountMgr.getActiveAccountByName(cmd.getAccountName(), cmd.getDomainId()); + if (target == null) { + return new ListResponse<>(); // Account not found + } + accountIdFilter = target.getId(); + } + } else { + // Regular User / Domain Admin: STRICTLY restricted to their own account + accountIdFilter = caller.getId(); + } + + Filter searchFilter = new Filter(DnsServerVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + Pair, Integer> result = dnsServerDao.searchDnsServers(cmd.getId(), cmd.getKeyword(), + cmd.getProvider(), accountIdFilter, searchFilter); + + ListResponse response = new ListResponse<>(); + List serverResponses = new ArrayList<>(); + for (DnsServerVO server : result.first()) { + serverResponses.add(createDnsServerResponse(server)); + } + response.setResponses(serverResponses, result.second()); + return response; + } + + @Override + public DnsServer updateDnsServer(UpdateDnsServerCmd cmd) { + Long dnsServerId = cmd.getId(); + DnsServerVO dnsServer = dnsServerDao.findById(dnsServerId); + if (dnsServer == null) { + 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 update this DNS server."); + } + + boolean validationRequired = false; + String originalUrl = dnsServer.getUrl(); + String originalKey = dnsServer.getApiKey(); + + if (cmd.getName() != null) { + dnsServer.setName(cmd.getName()); + } + + if (cmd.getUrl() != null) { + if (!cmd.getUrl().equals(originalUrl)) { + DnsServer duplicate = dnsServerDao.findByUrlAndAccount(cmd.getUrl(), dnsServer.getAccountId()); + if (duplicate != null && duplicate.getId() != dnsServer.getId()) { + throw new InvalidParameterValueException("Another DNS Server with this URL already exists."); + } + dnsServer.setUrl(cmd.getUrl()); + validationRequired = true; + } + } + + if (cmd.getCredentials() != null && !cmd.getCredentials().equals(originalKey)) { + dnsServer.setApiKey(cmd.getCredentials()); + validationRequired = true; + } + + if (cmd.getPort() != null) { + dnsServer.setPort(cmd.getPort()); + } + if (cmd.isPublic() != null) { + dnsServer.setIsPublic(cmd.isPublic()); + } + + if (cmd.getPublicDomainSuffix() != null) { + dnsServer.setPublicDomainSuffix(cmd.getPublicDomainSuffix()); + } + + if (cmd.getNameServers() != null) { + dnsServer.setNameServers(cmd.getNameServers()); + } + if (validationRequired) { + DnsProvider provider = getProvider(dnsServer.getProviderType()); + try { + provider.validate(dnsServer); + } catch (Exception ex) { + logger.error("Validation failed for DNS server", ex); + throw new InvalidParameterValueException("Validation failed for DNS server"); + } + } + boolean updateStatus = dnsServerDao.update(dnsServerId, dnsServer); + + if (updateStatus) { + return dnsServerDao.findById(dnsServerId); + } else { + throw new CloudRuntimeException(String.format("Unable to update DNS server: %s", dnsServer.getName())); + } } @Override public boolean deleteDnsServer(DeleteDnsServerCmd cmd) { - return false; + Long dnsServerId = cmd.getId(); + DnsServerVO dnsServer = dnsServerDao.findById(dnsServerId); + if (dnsServer == null) { + 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."); + } + return dnsServerDao.remove(dnsServerId); } @Override public DnsServerResponse createDnsServerResponse(DnsServer server) { - return null; + DnsServerResponse response = new DnsServerResponse(); + response.setId(server.getUuid()); + response.setName(server.getName()); + response.setUrl(server.getUrl()); + response.setProvider(server.getProviderType()); + response.setPublic(server.isPublic()); + response.setObjectName("dnsserver"); + return response; } @Override @@ -155,11 +299,13 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa @Override public List> getCommands() { List> cmdList = new ArrayList<>(); + + cmdList.add(ListDnsProvidersCmd.class); // DNS Server Commands cmdList.add(AddDnsServerCmd.class); cmdList.add(ListDnsServersCmd.class); cmdList.add(DeleteDnsServerCmd.class); - cmdList.add(ListDnsProvidersCmd.class); + cmdList.add(UpdateDnsServerCmd.class); // DNS Zone Commands cmdList.add(CreateDnsZoneCmd.class); diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java index 5fada010f62..0c64f742b63 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java @@ -19,12 +19,18 @@ package org.apache.cloudstack.dns.dao; import java.util.List; +import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.dns.vo.DnsServerVO; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; public interface DnsServerDao extends GenericDao { - List listByProviderType(String providerType); + List listByProvider(String provider); + DnsServer findByUrlAndAccount(String url, long accountId); + + Pair, Integer> searchDnsServers(Long id, String keyword, String provider, Long accountId, Filter filter); } diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java index 8967aa25d52..e9deb9bb2ca 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java @@ -19,29 +19,74 @@ package org.apache.cloudstack.dns.dao; import java.util.List; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.dns.vo.DnsServerVO; import org.springframework.stereotype.Component; +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @Component public class DnsServerDaoImpl extends GenericDaoBase implements DnsServerDao { - static final String PROVIDER_TYPE = "providerType"; + SearchBuilder AllFieldsSearch; SearchBuilder ProviderSearch; + SearchBuilder AccountUrlSearch; + public DnsServerDaoImpl() { super(); ProviderSearch = createSearchBuilder(); - ProviderSearch.and(PROVIDER_TYPE, ProviderSearch.entity().getProviderType(), SearchCriteria.Op.EQ); + ProviderSearch.and(ApiConstants.PROVIDER_TYPE, ProviderSearch.entity().getProviderType(), SearchCriteria.Op.EQ); ProviderSearch.done(); + + AccountUrlSearch = createSearchBuilder(); + AccountUrlSearch.and(ApiConstants.URL, AccountUrlSearch.entity().getUrl(), SearchCriteria.Op.EQ); + AccountUrlSearch.and(ApiConstants.ACCOUNT_ID, AccountUrlSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountUrlSearch.done(); + + AllFieldsSearch = createSearchBuilder(); + AllFieldsSearch.and(ApiConstants.ID, AllFieldsSearch.entity().getId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and(ApiConstants.NAME, AllFieldsSearch.entity().getName(), SearchCriteria.Op.LIKE); + AllFieldsSearch.and(ApiConstants.PROVIDER_TYPE, AllFieldsSearch.entity().getProviderType(), SearchCriteria.Op.EQ); + AllFieldsSearch.and(ApiConstants.ACCOUNT_ID, AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AllFieldsSearch.done(); + } @Override - public List listByProviderType(String providerType) { + public List listByProvider(String providerType) { SearchCriteria sc = ProviderSearch.create(); - sc.setParameters(PROVIDER_TYPE, providerType); + sc.setParameters(ApiConstants.PROVIDER_TYPE, providerType); return listBy(sc); } + + @Override + public DnsServer findByUrlAndAccount(String url, long accountId) { + SearchCriteria sc = AccountUrlSearch.create(); + sc.setParameters(ApiConstants.URL, url); + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + return findOneBy(sc); + } + + @Override + public Pair, Integer> searchDnsServers(Long id, String keyword, String provider, Long accountId, Filter filter) { + SearchCriteria sc = AllFieldsSearch.create(); + if (id != null) { + sc.setParameters(ApiConstants.ID, id); + } + if (keyword != null) { + sc.setParameters(ApiConstants.NAME, "%" + keyword + "%"); + } + if (provider != null) { + sc.setParameters(ApiConstants.PROVIDER_TYPE, provider); + } + if (accountId != null) { + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + } + return searchAndCount(sc, filter); + } } diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java index 0b80f474a52..4e216f8fac4 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java @@ -54,6 +54,9 @@ public class DnsServerVO implements DnsServer { @Column(name = "url") private String url; + @Column(name = "port") + private Integer port; + @Column(name = "provider_type") @Enumerated(EnumType.STRING) private DnsProviderType providerType; @@ -75,6 +78,9 @@ public class DnsServerVO implements DnsServer { @Column(name = "account_id") private long accountId; + @Column(name = "name_servers") + private String nameServers; + @Column(name = GenericDao.CREATED_COLUMN) @Temporal(value = TemporalType.TIMESTAMP) private Date created = null; @@ -83,19 +89,25 @@ public class DnsServerVO implements DnsServer { @Temporal(value = TemporalType.TIMESTAMP) private Date removed = null; - public DnsServerVO() { + DnsServerVO() { this.uuid = UUID.randomUUID().toString(); this.created = new Date(); } - public DnsServerVO(String name, String url, DnsProviderType providerType, String apiKey, long accountId, boolean isPublic) { + public DnsServerVO(String name, String url, DnsProviderType providerType, String apiKey, + Integer port, boolean isPublic, String publicDomainSuffix, String nameServers, + long accountId) { this(); this.name = name; this.url = url; + this.port = port; this.providerType = providerType; this.apiKey = apiKey; this.accountId = accountId; + this.publicDomainSuffix = publicDomainSuffix; + this.nameServers = nameServers; this.isPublic = isPublic; + this.state = State.Enabled; } @Override @@ -119,7 +131,7 @@ public class DnsServerVO implements DnsServer { } @Override - public String getAPIKey() { + public String getApiKey() { return apiKey; } @@ -154,4 +166,42 @@ public class DnsServerVO implements DnsServer { public void setState(State state) { this.state = state; } + + @Override + public String toString() { + return "DnsServerVO {" + + "id=" + id + + ", name='" + name + '\'' + + ", url='" + url + '\'' + + ", apiKey='*****'" + + "}"; + } + + public void setNameServers(String nameServers) { + this.nameServers = nameServers; + } + + public void setIsPublic(boolean value) { + isPublic = value; + } + + public void setPublicDomainSuffix(String publicDomainSuffix) { + this.publicDomainSuffix = publicDomainSuffix; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public void setPort(Integer port) { + this.port = port; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setName(String name) { + this.name = name; + } }