diff --git a/api/src/main/java/com/cloud/configuration/Resource.java b/api/src/main/java/com/cloud/configuration/Resource.java index 97be7f9d64c5..41abce02eb47 100644 --- a/api/src/main/java/com/cloud/configuration/Resource.java +++ b/api/src/main/java/com/cloud/configuration/Resource.java @@ -38,7 +38,8 @@ enum ResourceType { // All storage type resources are allocated_storage and not backup_storage("backup_storage", 13), bucket("bucket", 14), object_storage("object_storage", 15), - gpu("gpu", 16); + gpu("gpu", 16), + dns_zone("dns_zone", 17); private String name; private int ordinal; diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 889e821a0905..a770cba2a9ee 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -30,6 +30,9 @@ import org.apache.cloudstack.backup.BackupRepositoryService; import org.apache.cloudstack.config.Configuration; import org.apache.cloudstack.datacenter.DataCenterIpv4GuestSubnet; +import org.apache.cloudstack.dns.DnsRecord; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.DnsZone; import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.extension.ExtensionCustomAction; import org.apache.cloudstack.gpu.GpuCard; @@ -859,6 +862,16 @@ public class EventTypes { public static final String EVENT_BACKUP_REPOSITORY_ADD = "BACKUP.REPOSITORY.ADD"; public static final String EVENT_BACKUP_REPOSITORY_UPDATE = "BACKUP.REPOSITORY.UPDATE"; + // DNS Framework Events + public static final String EVENT_DNS_SERVER_ADD = "DNS.SERVER.ADD"; + public static final String EVENT_DNS_SERVER_UPDATE = "DNS.SERVER.UPDATE"; + public static final String EVENT_DNS_SERVER_DELETE = "DNS.SERVER.DELETE"; + public static final String EVENT_DNS_ZONE_CREATE = "DNS.ZONE.CREATE"; + public static final String EVENT_DNS_ZONE_UPDATE = "DNS.ZONE.UPDATE"; + public static final String EVENT_DNS_ZONE_DELETE = "DNS.ZONE.DELETE"; + public static final String EVENT_DNS_RECORD_CREATE = "DNS.RECORD.CREATE"; + public static final String EVENT_DNS_RECORD_DELETE = "DNS.RECORD.DELETE"; + static { // TODO: need a way to force author adding event types to declare the entity details as well, with out braking @@ -1397,6 +1410,15 @@ public class EventTypes { // Backup Repository entityEventDetails.put(EVENT_BACKUP_REPOSITORY_ADD, BackupRepositoryService.class); entityEventDetails.put(EVENT_BACKUP_REPOSITORY_UPDATE, BackupRepositoryService.class); + + // DNS Framework Events + entityEventDetails.put(EVENT_DNS_SERVER_ADD, DnsServer.class); + entityEventDetails.put(EVENT_DNS_SERVER_DELETE, DnsServer.class); + entityEventDetails.put(EVENT_DNS_ZONE_CREATE, DnsZone.class); + entityEventDetails.put(EVENT_DNS_ZONE_DELETE, DnsZone.class); + entityEventDetails.put(EVENT_DNS_RECORD_CREATE, DnsRecord.class); + entityEventDetails.put(EVENT_DNS_RECORD_DELETE, DnsRecord.class); + } public static boolean isNetworkEvent(String eventType) { diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index eb47b75ac5ba..bb33aedcbcdb 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.Map; -import com.cloud.utils.Pair; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -27,6 +26,9 @@ import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; import org.apache.cloudstack.api.command.admin.user.RegisterUserKeyCmd; import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.dns.DnsServer; import com.cloud.dc.DataCenter; import com.cloud.domain.Domain; @@ -35,8 +37,7 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; -import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; -import org.apache.cloudstack.backup.BackupOffering; +import com.cloud.utils.Pair; public interface AccountService { @@ -119,6 +120,8 @@ User createUser(String userName, String password, String firstName, String lastN void checkAccess(Account account, BackupOffering bof) throws PermissionDeniedException; + void checkAccess(Account account, DnsServer dnsServer) throws PermissionDeniedException; + void checkAccess(User user, ControlledEntity entity); void checkAccess(Account account, AccessType accessType, boolean sameOwner, String apiName, ControlledEntity... entities) throws PermissionDeniedException; diff --git a/api/src/main/java/com/cloud/user/ResourceLimitService.java b/api/src/main/java/com/cloud/user/ResourceLimitService.java index 738e593582b4..0622c5eb0806 100644 --- a/api/src/main/java/com/cloud/user/ResourceLimitService.java +++ b/api/src/main/java/com/cloud/user/ResourceLimitService.java @@ -56,6 +56,8 @@ public interface ResourceLimitService { "The default maximum number of GPU devices that can be used for a domain", false); static final ConfigKey DefaultMaxProjectGpus = new ConfigKey<>("Project Defaults",Long.class,"max.project.gpus","20", "The default maximum number of GPU devices that can be used for a project", false); + ConfigKey DefaultMaxDnsAccounts = new ConfigKey<>("Account Defaults",Long.class, "max.account.dns_zones","10", + "The default maximum number of DNS zones that can be created by an Account", true); static final List HostTagsSupportingTypes = List.of(ResourceType.user_vm, ResourceType.cpu, ResourceType.memory, ResourceType.gpu); static final List StorageTagsSupportingTypes = List.of(ResourceType.volume, ResourceType.primary_storage); 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 9a8913da5b04..1644960bdb08 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1334,6 +1334,29 @@ 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 DNS_USER_NAME = "dnsusername"; + 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 NATIVE_ZONE = "Native"; + public static final String NIC_DNS_RECORD = "nicdnsrecord"; + + 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/BaseCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java index f30f6f327826..9847d6072a44 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseCmd.java @@ -40,6 +40,7 @@ import org.apache.cloudstack.alert.AlertService; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsProviderManager; import org.apache.cloudstack.gpu.GpuService; import org.apache.cloudstack.network.RoutedIpv4Manager; import org.apache.cloudstack.network.lb.ApplicationLoadBalancerService; @@ -230,6 +231,9 @@ public static enum CommandType { @Inject public RoutedIpv4Manager routedIpv4Manager; + @Inject + public DnsProviderManager dnsProviderManager; + public abstract void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException; 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 new file mode 100644 index 000000000000..40464096181b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -0,0 +1,150 @@ +// 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 java.util.List; + +import org.apache.cloudstack.acl.RoleType; +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.DnsProviderType; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.commons.lang3.BooleanUtils; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "addDnsServer", + description = "Adds a new external DNS server", + responseObject = DnsServerResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class AddDnsServerCmd extends BaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + /// + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "Name of the DNS server") + private String name; + + @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "API URL of the provider") + private String url; + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, required = true, description = "Provider type (e.g., PowerDNS)") + private String provider; + + @Parameter(name = ApiConstants.DNS_USER_NAME, type = CommandType.STRING, + description = "Username or email associated with the external DNS provider account (used for authentication)") + private String dnsUserName; + + @Parameter(name = ApiConstants.CREDENTIALS, required = true, type = CommandType.STRING, description = "API key or credentials for the external provider") + private String credentials; + + @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") + 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.LIST, collectionType = CommandType.STRING, + required = true, description = "Comma separated list of name servers") + private List nameServers; + + @Parameter(name = "externalserverid", type = CommandType.STRING, description = "External server id or hostname for the DNS server, e.g., 'localhost' for PowerDNS") + private String externalServerId; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + 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 List getNameServers() { + return nameServers; + } + + public DnsProviderType getProvider() { + DnsProviderType dnsProviderType = EnumUtils.getEnumIgnoreCase(DnsProviderType.class, provider, DnsProviderType.PowerDNS); + if (dnsProviderType == null) { + throw new InvalidParameterValueException(String.format("Invalid value passed for provider type, valid values are: %s", + EnumUtils.listValues(DnsProviderType.values()))); + } + return dnsProviderType; + } + + @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()); + } + } + + public String getExternalServerId() { + return externalServerId; + } + + public String getDnsUserName() { + return dnsUserName; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java new file mode 100644 index 000000000000..53c697db9f28 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AssociateDnsZoneToNetworkCmd.java @@ -0,0 +1,94 @@ +// 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 org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +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.DnsZoneNetworkMapResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.NetworkResponse; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; + +@APICommand(name = "associateDnsZoneToNetwork", + description = "Associates a DNS Zone with a Network for VM auto-registration", + responseObject = DnsZoneNetworkMapResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class AssociateDnsZoneToNetworkCmd extends BaseCmd { + + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "The ID of the DNS zone") + private Long dnsZoneId; + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, + required = true, description = "The ID of the network") + private Long networkId; + + @Parameter(name = "subdomain", type = CommandType.STRING, + description = "Optional subdomain to append (e.g., 'dev' creates vm1.dev.example.com)") + private String subDomain; + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + DnsZoneNetworkMapResponse response = dnsProviderManager.associateZoneToNetwork(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + DnsZone zone = _entityMgr.findById(DnsZone.class, dnsZoneId); + if (zone != null) { + return zone.getAccountId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getDnsZoneId() { + return dnsZoneId; + } + + public Long getNetworkId() { + return networkId; + } + + public String getSubDomain() { + return subDomain; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java new file mode 100644 index 000000000000..ab00c8d8c7eb --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsRecordCmd.java @@ -0,0 +1,100 @@ +// 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 java.util.List; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsRecord; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "createDnsRecord", + description = "Creates a DNS record directly on the provider", + responseObject = DnsRecordResponse.class, + entityType = {DnsRecord.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class CreateDnsRecordCmd extends BaseAsyncCmd { + + @ACL + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, + description = "ID of the DNS zone") + private Long dnsZoneId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "Record name") + private String name; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "Record type (A, CNAME)") + private String type; + + @Parameter(name = ApiConstants.CONTENTS, type = CommandType.LIST, collectionType = CommandType.STRING, required = true, + description = "The content of the record (IP address for A/AAAA, FQDN for CNAME/NS, quoted string for TXT, etc.)") + private List contents; + + @Parameter(name = "ttl", type = CommandType.INTEGER, description = "Time to live") + private Integer ttl; + + // Getters + public Long getDnsZoneId() { return dnsZoneId; } + public String getName() { return name; } + + public List getContents() { return contents; } + public Integer getTtl() { return (ttl == null) ? 3600 : ttl; } + + public DnsRecord.RecordType getType() { + DnsRecord.RecordType dnsRecordType = EnumUtils.getEnumIgnoreCase(DnsRecord.RecordType.class, type); + if (dnsRecordType == null) { + throw new InvalidParameterValueException("Invalid value passed for record type, valid values are: " + EnumUtils.listValues(DnsRecord.RecordType.values())); + } + return dnsRecordType; + } + + @Override + public void execute() { + try { + DnsRecordResponse response = dnsProviderManager.createDnsRecord(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create DNS Record: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + + @Override + public String getEventType() { return EventTypes.EVENT_DNS_RECORD_CREATE; } + + @Override + public String getEventDescription() { return "Creating DNS Record: " + getName(); } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java new file mode 100644 index 000000000000..b69752e67b26 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java @@ -0,0 +1,145 @@ +// 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 java.util.Arrays; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCreateCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsZone; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.event.EventTypes; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "createDnsZone", + description = "Creates a new DNS Zone on a specific server", + responseObject = DnsZoneResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class CreateDnsZoneCmd extends BaseAsyncCreateCmd { + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, + description = "The name of the DNS zone (e.g. example.com)") + private String name; + + @ACL + @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; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, + description = "The type of zone (Public, Private). Defaults to Public.") + private String type; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "Display text for the zone") + private String description; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + public Long getDnsServerId() { + return dnsServerId; + } + + public DnsZone.ZoneType getType() { + if (StringUtils.isBlank(type)) { + return DnsZone.ZoneType.Public; + } + DnsZone.ZoneType zoneType = EnumUtils.getEnumIgnoreCase(DnsZone.ZoneType.class, type); + if (type == null) { + throw new IllegalArgumentException("Invalid type value, supported values are: " + Arrays.toString(DnsZone.ZoneType.values())); + } + return zoneType; + } + + public String getDescription() { + return description; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void create() throws ResourceAllocationException { + try { + DnsZone zone = dnsProviderManager.allocateDnsZone(this); + if (zone != null) { + setEntityId(zone.getId()); + setEntityUuid(zone.getUuid()); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create DNS Zone entity"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to allocate DNS Zone: " + e.getMessage()); + } + } + + @Override + public void execute() { + try { + DnsZone result = dnsProviderManager.provisionDnsZone(getEntityId()); + if (result != null) { + DnsZoneResponse response = dnsProviderManager.createDnsZoneResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to provision DNS Zone on external provider"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to provision DNS Zone: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_DNS_ZONE_CREATE; + } + + @Override + public String getEventDescription() { + return "creating DNS zone: " + getName(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java new file mode 100644 index 000000000000..1d7914c346e9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsRecordCmd.java @@ -0,0 +1,92 @@ +// 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 org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsRecord; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "deleteDnsRecord", + description = "Deletes a DNS record from the external provider", + responseObject = SuccessResponse.class, + entityType = {DnsRecord.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteDnsRecordCmd extends BaseAsyncCmd { + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "The ID of the DNS zone") + private Long dnsZoneId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true) + private String name; + + @Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true) + private String type; + + // Getters + public DnsRecord.RecordType getType() { + DnsRecord.RecordType dnsRecordType = EnumUtils.getEnumIgnoreCase(DnsRecord.RecordType.class, type); + if (dnsRecordType == null) { + throw new InvalidParameterValueException("Invalid value passed for record type, valid values are: " + EnumUtils.listValues(DnsRecord.RecordType.values())); + } + return dnsRecordType; + } + public Long getDnsZoneId() { return dnsZoneId; } + public String getName() { return name; } + + @Override + public void execute() { + try { + boolean result = dnsProviderManager.deleteDnsRecord(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS Record"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Error deleting DNS Record: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); } + + @Override + public String getEventType() { return EventTypes.EVENT_DNS_RECORD_DELETE; } + + @Override + public String getEventDescription() { return "Deleting DNS Record: " + getName(); } +} 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 new file mode 100644 index 000000000000..2fb8a9903e4c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsServerCmd.java @@ -0,0 +1,96 @@ +// 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 org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.dns.DnsServer; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; + +@APICommand(name = "deleteDnsServer", + description = "Removes a DNS server integration", + responseObject = SuccessResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteDnsServerCmd extends BaseAsyncCmd { + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, + required = true, description = "the ID of the DNS server") + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + try { + boolean result = dnsProviderManager.deleteDnsServer(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS server"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS server: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + DnsServer server = _entityMgr.findById(DnsServer.class, id); + if (server != null) { + return server.getAccountId(); + } + // If server not found, return System to fail safely (or let manager handle 404) + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public String getEventType() { return EventTypes.EVENT_DNS_SERVER_DELETE; } + + @Override + public String getEventDescription() { return "Deleting DNS server ID: " + getId(); } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java new file mode 100644 index 000000000000..fa8319a5ea20 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DeleteDnsZoneCmd.java @@ -0,0 +1,100 @@ +// 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 org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.event.EventTypes; +import com.cloud.user.Account; + +@APICommand(name = "deleteDnsZone", + description = "Removes a DNS Zone from CloudStack and the external provider", + responseObject = SuccessResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DeleteDnsZoneCmd extends BaseAsyncCmd { + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, + description = "The ID of the DNS zone") + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + try { + boolean result = dnsProviderManager.deleteDnsZone(getId()); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS Zone"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete DNS Zone: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + DnsZone zone = _entityMgr.findById(DnsZone.class, id); + if (zone != null) { + return zone.getAccountId(); + } + // Fallback or System if not found (likely to fail in execute() anyway) + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_DNS_ZONE_DELETE; + } + + @Override + public String getEventDescription() { + return "Deleting DNS Zone ID: " + getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java new file mode 100644 index 000000000000..51808323b7c2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/DisassociateDnsZoneFromNetworkCmd.java @@ -0,0 +1,83 @@ +// 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 org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +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.DnsZoneResponse; +import org.apache.cloudstack.api.response.NetworkResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; + +@APICommand(name = "disassociateDnsZoneFromNetwork", + description = "Removes the association between a DNS Zone and a Network", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class DisassociateDnsZoneFromNetworkCmd extends BaseCmd { + + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, description = "The ID of the DNS zone") + private Long dnsZoneId; + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @Parameter(name = ApiConstants.NETWORK_ID, type = CommandType.UUID, entityType = NetworkResponse.class, + required = true, description = "The ID of the Network") + private Long networkId; + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + try { + boolean result = dnsProviderManager.disassociateZoneFromNetwork(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to disassociate DNS zone from network."); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + public Long getDnsZoneId() { + return dnsZoneId; + } + + public Long getNetworkId() { + return networkId; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java new file mode 100644 index 000000000000..800f030754b9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsProvidersCmd.java @@ -0,0 +1,53 @@ +// 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 java.util.ArrayList; +import java.util.List; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.response.DnsProviderResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsProvider; + +@APICommand(name = "listDnsProviders", + description = "Lists available DNS plugin providers", + responseObject = DnsProviderResponse.class, + entityType = {DnsProvider.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListDnsProvidersCmd extends BaseListCmd { + + @Override + public void execute() { + List providers = dnsProviderManager.listProviderNames(); + ListResponse response = new ListResponse<>(); + List responses = new ArrayList<>(); + for (String name : providers) { + DnsProviderResponse resp = new DnsProviderResponse(name); + resp.setName(name); + responses.add(resp); + } + response.setResponses(responses); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java new file mode 100644 index 000000000000..246c8bee9aab --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsRecordsCmd.java @@ -0,0 +1,54 @@ +// 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 org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsRecord; + +@APICommand(name = "listDnsRecords", + description = "Lists DNS records from the external provider", + responseObject = DnsRecordResponse.class, + entityType = {DnsRecord.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListDnsRecordsCmd extends BaseListCmd { + + @Parameter(name = ApiConstants.DNS_ZONE_ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, required = true, + description = "ID of the DNS zone to list records from") + private Long dnsZoneId; + + public Long getDnsZoneId() { + return dnsZoneId; + } + + @Override + public void execute() { + // The manager will fetch live data from the plugin + ListResponse response = dnsProviderManager.listDnsRecords(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} 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 new file mode 100644 index 000000000000..ca7ac1944a9d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsServersCmd.java @@ -0,0 +1,82 @@ +// 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 org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListAccountResourcesCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.DnsServer; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "listDnsServers", + description = "Lists DNS servers owned by the account.", + responseObject = DnsServerResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListDnsServersCmd extends BaseListAccountResourcesCmd { + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsServerResponse.class, + description = "the ID of the DNS server") + private Long id; + + @Parameter(name = ApiConstants.PROVIDER_TYPE, type = CommandType.STRING, + description = "filter by provider type (e.g. PowerDNS, Cloudflare)") + private String providerType; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public DnsProviderType getProviderType() { + DnsProviderType dnsProviderType = EnumUtils.getEnumIgnoreCase(DnsProviderType.class, providerType, DnsProviderType.PowerDNS); + if (dnsProviderType == null) { + throw new InvalidParameterValueException(String.format("Invalid value passed for provider type, valid values are: %s", + EnumUtils.listValues(DnsProviderType.values()))); + } + return dnsProviderType; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + 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/ListDnsZonesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java new file mode 100644 index 000000000000..e71bdabaf617 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/ListDnsZonesCmd.java @@ -0,0 +1,63 @@ +// 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 org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +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.dns.DnsZone; + +@APICommand(name = "listDnsZones", + description = "Lists DNS zones.", responseObject = DnsZoneResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListDnsZonesCmd extends BaseListCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + description = "List DNS zone by ID") + private Long id; + + @Parameter(name = "dnsserverid", type = CommandType.UUID, entityType = DnsServerResponse.class, + description = "List DNS zones belonging to a specific DNS server") + private Long dnsServerId; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "List by zone name") + private String name; + + public Long getId() { return id; } + public Long getDnsServerId() { return dnsServerId; } + public String getName() { return name; } + + @Override + public void execute() { + ListResponse response = dnsProviderManager.listDnsZones(this); + response.setResponseName(getCommandName()); + 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 000000000000..52ba0497e743 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmd.java @@ -0,0 +1,137 @@ +// 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 org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.ACL; +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.dns.DnsServer; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; + +import com.cloud.user.Account; +import com.cloud.utils.EnumUtils; + +@APICommand(name = "updateDnsServer", + description = "Update DNS server", + responseObject = DnsServerResponse.class, + entityType = {DnsServer.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class UpdateDnsServerCmd extends BaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @ACL(accessType = SecurityChecker.AccessType.OperateEntry) + @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; + + @Parameter(name = ApiConstants.STATE, type = CommandType.STRING, description = "Update state for the DNS server (Enabled, Disabled)") + private String state; + + ///////////////////////////////////////////////////// + /////////////////// 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() { + DnsServer server = _entityMgr.findById(DnsServer.class, id); + if (server != null) { + return server.getAccountId(); + } + // If server not found, return System to fail safely (or let manager handle 404) + return Account.ACCOUNT_ID_SYSTEM; + } + + @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()); + } + } + + public DnsServer.State getState() { + if (StringUtils.isBlank(state)) { + return null; + } + DnsServer.State dnsState = EnumUtils.getEnumIgnoreCase(DnsServer.State.class, state); + if (dnsState == null) { + throw new IllegalArgumentException("Invalid state value: " + state); + } + return dnsState; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java new file mode 100644 index 000000000000..d4ebe71396c5 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsZoneCmd.java @@ -0,0 +1,87 @@ +// 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 org.apache.cloudstack.acl.RoleType; +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.DnsZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsZone; + +@APICommand(name = "updateDnsZone", + description = "Updates a DNS Zone's metadata", + responseObject = DnsZoneResponse.class, + entityType = {DnsZone.class}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + since = "4.23.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class UpdateDnsZoneCmd extends BaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API Parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = DnsZoneResponse.class, + required = true, description = "The ID of the DNS zone") + private Long id; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "Display text for the zone") + private String description; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getDescription() { + return description; + } + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// Implementation ////////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + try { + DnsZone result = dnsProviderManager.updateDnsZone(this); + if (result != null) { + DnsZoneResponse response = dnsProviderManager.createDnsZoneResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update DNS Zone on external provider"); + } + } catch (Exception e) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update DNS Zone: " + e.getMessage()); + } + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java new file mode 100644 index 000000000000..f3a571f40639 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsProviderResponse.java @@ -0,0 +1,45 @@ +// 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.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class DnsProviderResponse extends BaseResponse { + + @SerializedName(ApiConstants.NAME) + @Param(description = "The name of the DNS provider (e.g. PowerDNS, Cloudflare)") + private String name; + + public DnsProviderResponse(String name) { + this.name = name; + setObjectName("dnsprovider"); // Sets the JSON wrapper name + } + + // Accessors + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java new file mode 100644 index 000000000000..6e2082485512 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsRecordResponse.java @@ -0,0 +1,57 @@ +// 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.response; + +import java.util.List; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.dns.DnsRecord; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class DnsRecordResponse extends BaseResponse { + + @SerializedName(ApiConstants.NAME) + @Param(description = "The record name (e.g., www.example.com.)") + private String name; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "The record type (e.g., A, CNAME, TXT)") + private DnsRecord.RecordType type; + + @SerializedName("contents") + @Param(description = "The contents of the record (IP address or target)") + private List contents; + + @SerializedName("ttl") + @Param(description = "Time to live (TTL) in seconds") + private Integer ttl; + + public DnsRecordResponse() { + super(); + setObjectName("dnsrecord"); + } + + // Setters + public void setName(String name) { this.name = name; } + public void setType(DnsRecord.RecordType type) { this.type = type; } + public void setContent(List contents) { this.contents = contents; } + public void setTtl(Integer ttl) { this.ttl = ttl; } +} 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 new file mode 100644 index 000000000000..d67be31504df --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsServerResponse.java @@ -0,0 +1,125 @@ +// 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.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.DnsServer; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = DnsServer.class) +public class DnsServerResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the DNS server") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the DNS server") + private String name; + + @SerializedName(ApiConstants.URL) + @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; + + @SerializedName(ApiConstants.IS_PUBLIC) + @Param(description = "Is the DNS server publicly available") + private Boolean isPublic; + + @SerializedName(ApiConstants.PUBLIC_DOMAIN_SUFFIX) + @Param(description = "The public domain suffix for the DNS server") + private String publicDomainSuffix; + + @SerializedName(ApiConstants.NAME_SERVERS) + @Param(description = "Name servers entries associated to DNS server") + private List nameServers; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "the account associated with the DNS server") + private String accountName; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "the ID of the domain associated with the DNS server") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "the name of the domain associated with the DNS server") + private String domainName; + + public DnsServerResponse() { + super(); + + } + + 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(String 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; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneNetworkMapResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneNetworkMapResponse.java new file mode 100644 index 000000000000..84fe46ce5aca --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneNetworkMapResponse.java @@ -0,0 +1,64 @@ +// 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.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class DnsZoneNetworkMapResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "The ID of the mapping") + private String id; + + @SerializedName(ApiConstants.DNS_ZONE_ID) + @Param(description = "The ID of the DNS zone") + private String dnsZoneId; + + @SerializedName(ApiConstants.NETWORK_ID) + @Param(description = "The ID of the Network") + private String networkId; + + @SerializedName("subdomain") + @Param(description = "The sub domain name of the auto-registered DNS record") + private String subDomain; + + public DnsZoneNetworkMapResponse() { + super(); + setObjectName("dnszonenetwork"); + } + + // Setters + public void setId(String id) { + this.id = id; + } + + public void setDnsZoneId(String dnsZoneId) { + this.dnsZoneId = dnsZoneId; + } + + public void setNetworkId(String networkId) { + this.networkId = networkId; + } + + public void setSubDomain(String subDomain) { + this.subDomain = subDomain; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java new file mode 100644 index 000000000000..e179d2fd4b13 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/DnsZoneResponse.java @@ -0,0 +1,139 @@ +// 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.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = DnsZone.class) +public class DnsZoneResponse extends BaseResponse { + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the DNS zone") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the DNS zone") + private String name; + + @SerializedName("dnsserverid") + @Param(description = "ID of the DNS server this zone belongs to") + private String dnsServerId; + + @SerializedName("dnsservername") + @Param(description = "the name of the DNS server hosting this zone") + private String dnsServerName; + + @SerializedName("dnsserveraccount") + @Param(description = "the account name of the DNS server owner") + private String dnsServerAccountName; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "the account associated with the DNS zone") + private String accountName; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "the name of the domain associated with the DNS zone") + private String domainName; + + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "the ID of the domain associated with the DNS server") + private String domainId; + + @SerializedName(ApiConstants.NETWORK_ID) + @Param(description = "ID of the network this zone is associated with") + private String networkId; + + @SerializedName(ApiConstants.NETWORK_NAME) + @Param(description = "Name of the network this zone is associated with") + private String networkName; + + @SerializedName(ApiConstants.TYPE) + @Param(description = "The type of the zone (Public/Private)") + private DnsZone.ZoneType type; + + @SerializedName(ApiConstants.STATE) + @Param(description = "The state of the zone (Active/Inactive)") + private DnsZone.State state; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Description for the DNS zone") + private String description; + + public DnsZoneResponse() { + super(); + setObjectName("dnszone"); + } + + public void setName(String name) { + this.name = name; + } + + public void setDnsServerId(String dnsServerId) { + this.dnsServerId = dnsServerId; + } + + public void setNetworkId(String networkId) { + this.networkId = networkId; + } + + public void setNetworkName(String networkName) { + this.networkName = networkName; + } + + public void setType(DnsZone.ZoneType type) { + this.type = type; + } + + public void setState(DnsZone.State state) { + this.state = state; + } + + public void setId(String id) { + this.id = id; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setDnsServerName(String dnsServerName) { + this.dnsServerName = dnsServerName; + } + + public void setDnsServerAccountName(String dnsServerAccountName) { + this.dnsServerAccountName = dnsServerAccountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java index f992514b8db2..95b5fb401d22 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NicResponse.java @@ -146,6 +146,10 @@ public class NicResponse extends BaseResponse { @Param(description = "Public IP address associated with this NIC via Static NAT rule") private String publicIp; + @SerializedName("dnsrecordurl") + @Param(description = "Public IP address associated with this NIC via Static NAT rule") + private String dnsRecordUrl; + public void setVmId(String vmId) { this.vmId = vmId; } @@ -416,4 +420,8 @@ public void setPublicIpId(String publicIpId) { public void setPublicIp(String publicIp) { this.publicIp = publicIp; } + + public void setDnsRecordUrl(String dnsRecordUrl) { + this.dnsRecordUrl = dnsRecordUrl; + } } diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java new file mode 100644 index 000000000000..56e5c5b8abf2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProvider.java @@ -0,0 +1,44 @@ +// 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; + +import java.util.List; + +import org.apache.cloudstack.dns.exception.DnsProviderException; + +import com.cloud.utils.component.Adapter; + +public interface DnsProvider extends Adapter { + + DnsProviderType getProviderType(); + + // Validates connectivity to the server + void validate(DnsServer server) throws Exception; + + String validateAndResolveServer(DnsServer server) throws Exception; + + // Zone Operations + String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException; + void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException; + void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException; + + String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; + List listRecords(DnsServer server, DnsZone zone) throws DnsProviderException; + String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; + String deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException; +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java new file mode 100644 index 000000000000..b4dede12ce14 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderManager.java @@ -0,0 +1,78 @@ +// 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; + +import java.util.List; + +import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd; +import org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd; +import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsRecordCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.DisassociateDnsZoneFromNetworkCmd; +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.command.user.dns.UpdateDnsZoneCmd; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; + +import com.cloud.network.Network; +import com.cloud.utils.component.Manager; +import com.cloud.utils.component.PluggableService; +import com.cloud.vm.Nic; +import com.cloud.vm.VirtualMachine; + +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); + + // Allocates the DB row (State: Inactive) + DnsZone allocateDnsZone(CreateDnsZoneCmd cmd); + // Calls the Plugin (State: Inactive -> Active) + DnsZone provisionDnsZone(long zoneId); + + DnsZone updateDnsZone(UpdateDnsZoneCmd cmd); + boolean deleteDnsZone(Long id); + ListResponse listDnsZones(ListDnsZonesCmd cmd); + + DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd); + boolean deleteDnsRecord(DeleteDnsRecordCmd cmd); + ListResponse listDnsRecords(ListDnsRecordsCmd cmd); + + List listProviderNames(); + + // Helper to create the response object + DnsZoneResponse createDnsZoneResponse(DnsZone zone); + DnsRecordResponse createDnsRecordResponse(DnsRecord record); + + DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetworkCmd cmd); + + boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd); + + String processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd); +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java new file mode 100644 index 000000000000..23c8e936613f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsProviderType.java @@ -0,0 +1,23 @@ +// 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; + +public enum DnsProviderType { + PowerDNS; +// Cloudflare +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java b/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java new file mode 100644 index 000000000000..ae62e4729cca --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsRecord.java @@ -0,0 +1,66 @@ +// 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; + +import java.util.List; + +import com.cloud.utils.exception.CloudRuntimeException; + +public class DnsRecord { + + public enum RecordType { + A, AAAA, CNAME, MX, TXT, SRV, PTR, NS; + + public static RecordType fromString(String type) { + if (type == null) return null; + try { + return RecordType.valueOf(type.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new CloudRuntimeException("Invalid DNS Record Type: " + type + + ". Supported: " + java.util.Arrays.toString(values())); + } + } + } + + private String name; + private RecordType type; + private List contents; + private int ttl; + + public DnsRecord() {} + + public DnsRecord(String name, RecordType type, List contents, int ttl) { + this.name = name; + this.type = type; + this.contents = contents; + this.ttl = ttl; + } + + // Getters and Setters + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public RecordType getType() { return type; } + public void setType(RecordType type) { this.type = type; } + + public List getContents() { return contents; } + public void setContents(List contents) { this.contents = contents; } + + public int getTtl() { return ttl; } + public void setTtl(int ttl) { this.ttl = ttl; } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java new file mode 100644 index 000000000000..0261902ef98c --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsServer.java @@ -0,0 +1,55 @@ +// 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; + +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, ControlledEntity { + enum State { + Enabled, Disabled + }; + + String getName(); + + String getUrl(); + + DnsProviderType getProviderType(); + + List getNameServers(); + + String getApiKey(); + + long getAccountId(); + + boolean isPublicServer(); + + Date getCreated(); + + Date getRemoved(); + + String getPublicDomainSuffix(); + + String getExternalServerId(); + + Integer getPort(); +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java new file mode 100644 index 000000000000..193185a6b0c6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/DnsZone.java @@ -0,0 +1,47 @@ +// 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; + +import java.util.List; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface DnsZone extends InternalIdentity, Identity, ControlledEntity { + enum ZoneType { + Public, Private + } + enum State { + Active, Inactive + } + + String getName(); + + long getDnsServerId(); + + long getAccountId(); + + ZoneType getType(); + + String getDescription(); + + List getAssociatedNetworks(); + + State getState(); +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java new file mode 100644 index 000000000000..325cb78241ef --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsAuthenticationException.java @@ -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); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java new file mode 100644 index 000000000000..9a36bb87478a --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsConflictException.java @@ -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); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java new file mode 100644 index 000000000000..aa88f308ce84 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsNotFoundException.java @@ -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); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java new file mode 100644 index 000000000000..564acdc9a6f0 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsOperationException.java @@ -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); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java new file mode 100644 index 000000000000..de307c9903e4 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsProviderException.java @@ -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); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java new file mode 100644 index 000000000000..50f04143c921 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/dns/exception/DnsTransportException.java @@ -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); + } +} diff --git a/client/pom.xml b/client/pom.xml index b8dffe65d4fb..5c7abe771dce 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -662,6 +662,12 @@ cloud-utils ${project.version} + + org.apache.cloudstack + cloud-plugin-dns-powerdns + ${project.version} + + diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index 01c568d78916..8bca42f16bc0 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -366,4 +366,6 @@ + + diff --git a/core/src/main/resources/META-INF/cloudstack/dns/module.properties b/core/src/main/resources/META-INF/cloudstack/dns/module.properties new file mode 100644 index 000000000000..a2bb467be751 --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/dns/module.properties @@ -0,0 +1,21 @@ +# +# 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. +# + +name=dns +parent=core diff --git a/core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml b/core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml new file mode 100644 index 000000000000..27cac9400284 --- /dev/null +++ b/core/src/main/resources/META-INF/cloudstack/dns/spring-core-lifecycle-dns-context-inheritable.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 0656d5e3c440..191507e04563 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -310,4 +310,10 @@ + + + + + + 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 d330ecd0c0d5..fe76e949b7c6 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 @@ -49,3 +49,76 @@ CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` ( INDEX `i_webhook_filter__webhook_id`(`webhook_id`), CONSTRAINT `fk_webhook_filter__webhook_id` FOREIGN KEY(`webhook_id`) REFERENCES `webhook`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +-- ====================================================================== +-- DNS Framework Schema +-- ====================================================================== + +-- DNS Server Table (Stores DNS Server Configurations) +CREATE TABLE `cloud`.`dns_server` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns server', + `uuid` varchar(40) 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', + `dns_username` varchar(255) COMMENT 'username or email for dns server credentials', + `api_key` varchar(255) NOT NULL COMMENT 'dns server api_key', + `external_server_id` varchar(255) COMMENT 'dns server id e.g. localhost for powerdns', + `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', + `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)', + PRIMARY KEY (`id`), + KEY `i_dns_server__account_id` (`account_id`), + CONSTRAINT `fk_dns_server__account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- DNS Zone Table (Stores DNS Zone Metadata) +CREATE TABLE `cloud`.`dns_zone` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone', + `uuid` varchar(40) COMMENT 'uuid of the dns zone', + `name` varchar(255) NOT NULL COMMENT 'dns zone name (e.g. example.com)', + `dns_server_id` bigint unsigned NOT NULL COMMENT 'fk to dns_server.id', + `external_reference` VARCHAR(255) COMMENT 'id of external provider resource', + `domain_id` bigint unsigned COMMENT 'for domain-specific ownership', + `account_id` bigint unsigned COMMENT 'account id. foreign key to account table', + `description` varchar(1024) DEFAULT NULL, + `type` ENUM('Private', 'Public') NOT NULL DEFAULT 'Public', + `state` ENUM('Active', 'Inactive') NOT NULL DEFAULT 'Inactive', + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', + PRIMARY KEY (`id`), + CONSTRAINT `uc_dns_zone__uuid` UNIQUE (`uuid`), + KEY `i_dns_zone__dns_server` (`dns_server_id`), + KEY `i_dns_zone__account_id` (`account_id`), + CONSTRAINT `fk_dns_zone__dns_server_id` FOREIGN KEY (`dns_server_id`) REFERENCES `dns_server` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dns_zone__account_id` FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dns_zone__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- DNS Zone Network Map (One-to-Many Link) +CREATE TABLE `cloud`.`dns_zone_network_map` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the dns zone to network mapping', + `uuid` varchar(40), + `dns_zone_id` bigint(20) unsigned NOT NULL, + `network_id` bigint(20) unsigned NOT NULL COMMENT 'network to which dns zone is associated to', + `sub_domain` varchar(255) DEFAULT NULL COMMENT 'Subdomain for auto-registration', + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime DEFAULT NULL COMMENT 'Date removed (soft delete)', + PRIMARY KEY (`id`), + CONSTRAINT `uc_dns_zone__uuid` UNIQUE (`uuid`), + KEY `fk_dns_map__zone_id` (`dns_zone_id`), + KEY `fk_dns_map__network_id` (`network_id`), + CONSTRAINT `fk_dns_map__zone_id` FOREIGN KEY (`dns_zone_id`) REFERENCES `dns_zone` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dns_map__network_id` FOREIGN KEY (`network_id`) REFERENCES `networks` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Set default limit to 10 DNS zones for standard Accounts +INSERT INTO `cloud`.`configuration` (`category`, `instance`, `component`, `name`, `value`, `description`, `default_value`) +VALUES ('Advanced', 'DEFAULT', 'ResourceLimitManager', 'max.account.dns_zones', '10', 'The default maximum number of DNS zones that can be created by an Account', '10'); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_server_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_server_view.sql new file mode 100644 index 000000000000..a1bc1a1141a9 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_server_view.sql @@ -0,0 +1,44 @@ +-- 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. + +-- VIEW `cloud`.`dns_server_view`; + +DROP VIEW IF EXISTS `cloud`.`dns_server_view`; +CREATE VIEW `cloud`.`dns_server_view` AS + SELECT + dns.id, + dns.uuid, + dns.name, + dns.provider_type, + dns.url, + dns.port, + dns.name_servers, + dns.is_public, + dns.public_domain_suffix, + dns.state, + dns.created, + dns.removed, + account.account_name account_name, + domain.name domain_name, + domain.uuid domain_uuid, + domain.path domain_path + FROM + `cloud`.`dns_server` dns + INNER JOIN + `cloud`.`account` account ON dns.account_id = account.id + INNER JOIN + `cloud`.`domain` domain ON dns.domain_id = domain.id; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_zone_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_zone_view.sql new file mode 100644 index 000000000000..a41e003ae4f2 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.dns_zone_view.sql @@ -0,0 +1,45 @@ +-- 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. + +-- VIEW `cloud`.`dns_zone_view`; + +DROP VIEW IF EXISTS `cloud`.`dns_zone_view`; +CREATE VIEW `cloud`.`dns_zone_view` AS + SELECT + zone.id, + zone.uuid, + zone.name, + zone.dns_server_id, + zone.state, + zone.description, + server.uuid dns_server_uuid, + server.name dns_server_name, + server_account.account_name dns_server_account_name, + account.account_name account_name, + domain.name domain_name, + domain.uuid domain_uuid, + domain.path domain_path + FROM + `cloud`.`dns_zone` zone + INNER JOIN + `cloud`.`dns_server` server ON zone.dns_server_id = server.id + INNER JOIN + `cloud`.`account` server_account ON server.account_id = server_account.id + INNER JOIN + `cloud`.`account` account ON zone.account_id = account.id + INNER JOIN + `cloud`.`domain` domain ON zone.domain_id = domain.id; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql index 94bc8640fd54..ac2d007bd2bf 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.user_vm_view.sql @@ -141,6 +141,7 @@ SELECT `nics`.`mac_address` AS `mac_address`, `nics`.`broadcast_uri` AS `broadcast_uri`, `nics`.`isolation_uri` AS `isolation_uri`, + `nic_details`.`value` AS `dns_record_url`, `vpc`.`id` AS `vpc_id`, `vpc`.`uuid` AS `vpc_uuid`, `networks`.`uuid` AS `network_uuid`, @@ -185,7 +186,7 @@ SELECT `lease_expiry_action`.`value` AS `lease_expiry_action`, `lease_action_execution`.`value` AS `lease_action_execution` FROM - (((((((((((((((((((((((((((((((((((((`user_vm` + ((((((((((((((((((((((((((((((((((((((`user_vm` JOIN `vm_instance` ON (((`vm_instance`.`id` = `user_vm`.`id`) AND ISNULL(`vm_instance`.`removed`)))) JOIN `account` ON ((`vm_instance`.`account_id` = `account`.`id`))) @@ -212,6 +213,7 @@ FROM LEFT JOIN `user_data` ON ((`user_data`.`id` = `user_vm`.`user_data_id`))) LEFT JOIN `nics` ON (((`vm_instance`.`id` = `nics`.`instance_id`) AND ISNULL(`nics`.`removed`)))) + LEFT JOIN `nic_details` ON ((`nic_details`.`nic_id` = `nics`.`id`) AND (`nic_details`.`name` = 'nicdnsrecord'))) LEFT JOIN `networks` ON ((`nics`.`network_id` = `networks`.`id`))) LEFT JOIN `vpc` ON (((`networks`.`vpc_id` = `vpc`.`id`) AND ISNULL(`vpc`.`removed`)))) diff --git a/plugins/dns/powerdns/pom.xml b/plugins/dns/powerdns/pom.xml new file mode 100644 index 000000000000..ef915559f478 --- /dev/null +++ b/plugins/dns/powerdns/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + cloud-plugin-dns-powerdns + Apache CloudStack Plugin - PowerDNS + + org.apache.cloudstack + cloudstack-plugins + 4.23.0.0-SNAPSHOT + ../../pom.xml + + + + 11 + 11 + UTF-8 + + + 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 000000000000..4abe2b0b02db --- /dev/null +++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsClient.java @@ -0,0 +1,356 @@ +// 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 java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +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.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.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +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 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"; + public static final String DEFAULT_SERVER_NAME = "localhost"; + + private final CloseableHttpClient httpClient; + + public PowerDnsClient() { + 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() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .evictIdleConnections(30, TimeUnit.SECONDS) + .disableCookieManagement() + .build(); + } + + public String resolveServerId(String baseUrl, Integer port, String apiKey, String externalServerId) throws DnsProviderException { + if (StringUtils.isNotBlank(externalServerId)) { + return validateServerId(baseUrl, port, apiKey, externalServerId); + } + return discoverAuthoritativeServerId(baseUrl, port, apiKey); + } + + public String validateServerId(String baseUrl, Integer port, String apiKey, String externalServerId) throws DnsProviderException { + String encodedServer = URLEncoder.encode(externalServerId, StandardCharsets.UTF_8); + HttpGet request = new HttpGet(buildUrl(baseUrl, port, "/servers/" + encodedServer)); + JsonNode server = execute(request, apiKey, 200); + if (!ApiConstants.AUTHORITATIVE.equalsIgnoreCase(server.path("daemon_type").asText(null))) { + throw new DnsOperationException(String.format("Server %s is not authoritative type=%s", externalServerId, + server.path("daemon_type").asText(null))); + } + return externalServerId; + } + + public String discoverAuthoritativeServerId(String baseUrl, Integer port, String apiKey) throws DnsProviderException { + String url = buildUrl(baseUrl, port , "/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 fallbackId = null; + for (JsonNode server : servers) { + String daemonType = server.path("daemon_type").asText(null); + if (!ApiConstants.AUTHORITATIVE.equalsIgnoreCase(daemonType)) { + continue; + } + String serverId = server.path(ApiConstants.ID).asText(null); + if (StringUtils.isBlank(serverId)) { + continue; + } + // Prefer localhost if present + if (DEFAULT_SERVER_NAME.equals(serverId)) { + return serverId; + } + if (fallbackId == null) { + fallbackId = serverId; + } + } + if (fallbackId != null) { + return fallbackId; + } + throw new DnsOperationException("No authoritative PowerDNS server found"); + } + + public String createZone(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName, + String zoneKind, boolean dnsSecFlag, List nameServers) throws DnsProviderException { + + validateServerId(baseUrl, port, apiKey, externalServerId); + 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, port, "/servers/" + externalServerId + "/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, Integer port, String apiKey, String externalServerId, String zoneName, + String zoneKind, Boolean dnsSecFlag, List nameServers) throws DnsProviderException { + + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + String url = buildUrl(baseUrl, port,"/servers/" + externalServerId + "/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, Integer port, String apiKey, String externalServerId, String zoneName) throws DnsProviderException { + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + HttpDelete request = new HttpDelete(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/zones/" + encodedZone)); + execute(request, apiKey, 204, 404); + } + + public String modifyRecord(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName, + String recordName, String type, long ttl, List contents, String changeType) throws DnsProviderException { + + validateServerId(baseUrl, port, apiKey, externalServerId); + 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, port, "/servers/" + externalServerId + "/zones/" + encodedZone)); + request.setEntity(new org.apache.http.entity.StringEntity(root.toString(), StandardCharsets.UTF_8)); + execute(request, apiKey, 204); + return normalizedRecord.endsWith(".") ? normalizedRecord.substring(0, normalizedRecord.length() - 1) : normalizedRecord; + } + + public Iterable listRecords(String baseUrl, Integer port, String apiKey, String externalServerId, String zoneName) throws DnsProviderException { + validateServerId(baseUrl, port, apiKey, externalServerId); + String normalizedZone = normalizeZone(zoneName); + String encodedZone = URLEncoder.encode(normalizedZone, StandardCharsets.UTF_8); + HttpGet request = new HttpGet(buildUrl(baseUrl, port, "/servers/" + externalServerId + "/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, Integer port, String path) { + String fullUrl = normalizeBaseUrl(baseUrl); + if (port != null && port > 0) { + try { + URI uri = new URI(fullUrl); + if (uri.getPort() == -1) { + fullUrl = fullUrl + ":" + port; + } + } catch (URISyntaxException e) { + fullUrl = fullUrl + ":" + port; + } + } + if (!path.startsWith("/")) { + path = "/" + path; + } + return fullUrl + 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 + "."; + } + if (zone.length() < 2) { + throw new IllegalArgumentException("Zone name is too short"); + } + return zone; + } + + String normalizeRecordName(String recordName, String zoneName) { + if (recordName == null) { + throw new IllegalArgumentException("Record name must not be null"); + } + String normalizedZone = normalizeZone(zoneName); + String name = recordName.trim().toLowerCase(); + // Apex of the zone + if (name.equals("@") || name.isEmpty()) { + return normalizedZone; + } + + 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; + } + if (name.contains(".")) { + return name + "."; + } + // Relative name → append zone + return name + "." + normalizedZone; + } + + @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 new file mode 100644 index 000000000000..897488e5a576 --- /dev/null +++ b/plugins/dns/powerdns/src/main/java/org/apache/cloudstack/dns/powerdns/PowerDnsProvider.java @@ -0,0 +1,187 @@ +// 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.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.DnsProvider; +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; +import com.fasterxml.jackson.databind.JsonNode; + +public class PowerDnsProvider extends AdapterBase implements DnsProvider { + + private PowerDnsClient client; + + @Override + public DnsProviderType getProviderType() { + return DnsProviderType.PowerDNS; + } + + public void validate(DnsServer server) throws DnsProviderException { + validateRequiredServerFields(server); + client.validateServerId(server.getUrl(), server.getPort(), server.getApiKey(), server.getExternalServerId()); + } + + @Override + public String validateAndResolveServer(DnsServer server) throws Exception { + validateRequiredServerFields(server); + return client.resolveServerId(server.getUrl(), server.getPort(), server.getApiKey(), server.getExternalServerId()); + } + + @Override + public String provisionZone(DnsServer server, DnsZone zone) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + return client.createZone( + server.getUrl(), + server.getPort(), + server.getApiKey(), + server.getExternalServerId(), + zone.getName(), + ApiConstants.NATIVE_ZONE, false, server.getNameServers() + ); + } + + @Override + public void deleteZone(DnsServer server, DnsZone zone) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + client.deleteZone(server.getUrl(), server.getPort(), server.getApiKey(), server.getExternalServerId(), zone.getName()); + } + + @Override + public void updateZone(DnsServer server, DnsZone zone) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + client.updateZone( + server.getUrl(), + server.getPort(), + server.getApiKey(), + server.getExternalServerId(), + zone.getName(), ApiConstants.NATIVE_ZONE, false, server.getNameServers()); + } + + public enum ChangeType { + REPLACE, DELETE + } + + @Override + public String addRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + return applyRecord( + server.getUrl(), + server.getPort(), + server.getApiKey(), + server.getExternalServerId(), + zone.getName(), record, ChangeType.REPLACE); + } + + @Override + public String updateRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + return addRecord(server, zone, record); + } + + @Override + public String deleteRecord(DnsServer server, DnsZone zone, DnsRecord record) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + return applyRecord(server.getUrl(), + server.getPort(), + server.getApiKey(), + server.getExternalServerId(), + zone.getName(), record, ChangeType.DELETE); + } + + public String applyRecord(String serverUrl, Integer port, String apiKey, String externalServerId, String zoneName, + DnsRecord record, ChangeType changeType) throws DnsProviderException { + + return client.modifyRecord(serverUrl, port, apiKey, externalServerId, zoneName, record.getName(), + record.getType().name(), record.getTtl(), record.getContents(), changeType.name()); + } + + @Override + public List listRecords(DnsServer server, DnsZone zone) throws DnsProviderException { + validateRequiredServerAndZoneFields(server, zone); + List records = new ArrayList<>(); + Iterable rrsetNodes = client.listRecords(server.getUrl(), server.getPort(), server.getApiKey(), + server.getExternalServerId(), zone.getName()); + + for (JsonNode rrset : rrsetNodes) { + String name = rrset.path(ApiConstants.NAME).asText(); + String typeStr = rrset.path(ApiConstants.TYPE).asText(); + int ttl = rrset.path(ApiConstants.TTL).asInt(0); + if (!"SOA".equalsIgnoreCase(typeStr)) { + try { + List contents = new ArrayList<>(); + JsonNode recordsNode = rrset.path(ApiConstants.RECORDS); + if (recordsNode.isArray()) { + for (JsonNode rec : recordsNode) { + String content = rec.path(ApiConstants.CONTENT).asText(); + if (!content.isEmpty()) { + contents.add(content); + } + } + } + records.add(new DnsRecord(name, DnsRecord.RecordType.valueOf(typeStr), contents, ttl)); + } catch (Exception ignored) { + // Skip unsupported record types + } + } + } + return records; + } + + void validateRequiredServerAndZoneFields(DnsServer server, DnsZone zone) { + validateRequiredServerFields(server); + if (StringUtils.isBlank(zone.getName())) { + throw new IllegalArgumentException("Zone name cannot be empty"); + } + } + + void validateRequiredServerFields(DnsServer server) { + 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"); + } + } + + @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/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/module.properties b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/module.properties new file mode 100644 index 000000000000..baec3fde6a6a --- /dev/null +++ b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/module.properties @@ -0,0 +1,18 @@ +# 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. +name=powerdns +parent=dns diff --git a/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml new file mode 100644 index 000000000000..c9e4937dac55 --- /dev/null +++ b/plugins/dns/powerdns/src/main/resources/META-INF/cloudstack/powerdns/spring-dns-powerdns-context.xml @@ -0,0 +1,31 @@ + + + + + diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java new file mode 100644 index 000000000000..25d8f1583726 --- /dev/null +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java @@ -0,0 +1,101 @@ +// 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; + +import static org.apache.cloudstack.dns.DnsProviderUtil.appendPublicSuffixToZone; +import static org.apache.cloudstack.dns.DnsProviderUtil.normalizeDomain; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Collection; + +import org.apache.logging.log4j.util.Strings; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class DnsProviderUtilTest { + private final String userZoneName; + private final String publicSuffix; + private final String expectedResult; + private final boolean expectException; + + public DnsProviderUtilTest(String userZoneName, + String publicSuffix, + String expectedResult, + boolean expectException) { + this.userZoneName = userZoneName; + this.publicSuffix = publicSuffix; + this.expectedResult = expectedResult; + this.expectException = expectException; + } + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"tenant1.com", "example.com", "tenant1.example.com", false}, + {"dev.tenant2.com", "example.com", "dev.tenant2.example.com", false}, + {"tenant3.example.com", "example.com", "tenant3.example.com", false}, + {"Tenant1.CoM", "ExAmple.CoM", "tenant1.example.com", false}, + {"tenant1.com.", "example.com.", "tenant1.example.com", false}, + {"tenant1.com", "", "tenant1.com", false}, + {"tenant1.com", null, "tenant1.com", false}, + {"test.abc.com", "abc.com", "test.abc.com", false}, + {"sub.test.abc.com", "abc.com", "sub.test.abc.com", false}, + {"test.ai.abc.com", "abc.com", "test.ai.abc.com", false}, + {"deep.sub.abc.com", "abc.com", "deep.sub.abc.com", false}, + {"abc.com", "xyz.com", "abc.xyz.com", false}, + {"test.xyz.com", "xyz.com", "test.xyz.com", false}, + {"test.com.xyz.com", "xyz.com", "test.com.xyz.com", false}, + {"tenant", "example.com", null, true}, // single label + {"test", "abc.com", null, true}, + {"example.com.", "example.com", null, true}, + {"example.com", "example.com", null, true}, // root level forbidden + {"abc.com", "abc.com", null, true}, // root level forbidden + {"tenant1.org", "example.com", null, true}, // TLD mismatch + {"test.ai", "abc.com", null, true}, // TLD mismatch + {null, "example.com", null, true}, + }); + } + + @Test + public void testAppendPublicSuffix() { + if (expectException) { + try { + executeAppendSuffixTest(userZoneName, publicSuffix); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException ignored) { + // noop + } + } else { + String result; + if (Strings.isNotBlank(publicSuffix)) { + result = executeAppendSuffixTest(userZoneName, publicSuffix); + } else { + result = appendPublicSuffixToZone(normalizeDomain(userZoneName), publicSuffix); + } + assertEquals(expectedResult, result); + } + } + + String executeAppendSuffixTest(String zoneName, String domainSuffix) { + return appendPublicSuffixToZone(normalizeDomain(zoneName), domainSuffix); + } +} diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java new file mode 100644 index 000000000000..d0a73e48d87b --- /dev/null +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java @@ -0,0 +1,83 @@ +// 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); + } +} diff --git a/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/module.properties b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/module.properties new file mode 100644 index 000000000000..c33f70e8919a --- /dev/null +++ b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/module.properties @@ -0,0 +1,18 @@ +# 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. +name=inmemory +parent=event diff --git a/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/spring-event-inmemory-context.xml b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/spring-event-inmemory-context.xml new file mode 100644 index 000000000000..4b0215170639 --- /dev/null +++ b/plugins/event-bus/inmemory/src/main/resources/META-INF/cloudstack/inmemory/spring-event-inmemory-context.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/plugins/pom.xml b/plugins/pom.xml index e7d13871285e..acd5efad7de2 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -154,6 +154,7 @@ user-two-factor-authenticators/totp user-two-factor-authenticators/static-pin + dns/powerdns diff --git a/server/src/main/java/com/cloud/acl/DomainChecker.java b/server/src/main/java/com/cloud/acl/DomainChecker.java index 0500960abb13..9c8314dc252b 100644 --- a/server/src/main/java/com/cloud/acl/DomainChecker.java +++ b/server/src/main/java/com/cloud/acl/DomainChecker.java @@ -27,13 +27,13 @@ import org.apache.cloudstack.acl.RolePermissionEntity; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.affinity.AffinityGroup; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; import org.springframework.stereotype.Component; -import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; -import org.apache.cloudstack.backup.BackupOffering; import com.cloud.dc.DataCenter; import com.cloud.dc.DedicatedResourceVO; import com.cloud.dc.dao.DedicatedResourceDao; diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 93dca8cc07a1..0d09083ccdfe 100644 --- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -17,14 +17,13 @@ package com.cloud.api.query.dao; import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Collections; import java.time.LocalDate; import java.time.ZoneId; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.Date; - import java.util.HashMap; import java.util.Hashtable; import java.util.List; @@ -34,8 +33,6 @@ import javax.inject.Inject; -import com.cloud.gpu.dao.VgpuProfileDao; -import com.cloud.service.dao.ServiceOfferingDao; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; @@ -61,11 +58,13 @@ import com.cloud.api.ApiResponseHelper; import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.gpu.GPU; +import com.cloud.gpu.dao.VgpuProfileDao; import com.cloud.host.ControlState; import com.cloud.network.IpAddress; import com.cloud.network.vpc.VpcVO; import com.cloud.network.vpc.dao.VpcDao; import com.cloud.service.ServiceOfferingDetailsVO; +import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOS; import com.cloud.storage.Storage.TemplateType; @@ -92,9 +91,9 @@ import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; import com.cloud.vm.VmStats; +import com.cloud.vm.dao.NicDetailsDao; import com.cloud.vm.dao.NicExtraDhcpOptionDao; import com.cloud.vm.dao.NicSecondaryIpVO; - import com.cloud.vm.dao.VMInstanceDetailsDao; @Component @@ -128,6 +127,8 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation VmDetailSearch; private final SearchBuilder activeVmByIsoSearch; @@ -358,6 +359,7 @@ public UserVmResponse newUserVmResponse(ResponseView view, String objectName, Us nicResponse.setIp6Address(userVm.getIp6Address()); nicResponse.setIp6Gateway(userVm.getIp6Gateway()); nicResponse.setIp6Cidr(userVm.getIp6Cidr()); + nicResponse.setDnsRecordUrl(userVm.getDnsRecordUrl()); if (userVm.getBroadcastUri() != null) { nicResponse.setBroadcastUri(userVm.getBroadcastUri().toString()); } @@ -611,6 +613,9 @@ public UserVmResponse setUserVmResponse(ResponseView view, UserVmResponse userVm nicResponse.setIp6Gateway(uvo.getIp6Gateway()); /*13: IPv6Cidr*/ nicResponse.setIp6Cidr(uvo.getIp6Cidr()); + /* dnsRecordUrl */ + nicResponse.setDnsRecordUrl(uvo.getDnsRecordUrl()); + /*14: deviceId*/ // where do we find nicResponse.setDeviceId( // this is probably not String.valueOf(uvo.getNicId())); as this is a db-id diff --git a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java index eab34081d514..1a6dbffe1897 100644 --- a/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/UserVmJoinVO.java @@ -32,21 +32,22 @@ import javax.persistence.TemporalType; import javax.persistence.Transient; +import org.apache.cloudstack.util.HypervisorTypeConverter; + import com.cloud.host.Status; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.network.Network.GuestType; import com.cloud.network.Networks.TrafficType; import com.cloud.resource.ResourceState; import com.cloud.storage.Storage; -import com.cloud.storage.Storage.TemplateType; import com.cloud.storage.Storage.StoragePoolType; +import com.cloud.storage.Storage.TemplateType; import com.cloud.storage.Volume; import com.cloud.user.Account; import com.cloud.util.StoragePoolTypeConverter; import com.cloud.utils.db.GenericDao; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; -import org.apache.cloudstack.util.HypervisorTypeConverter; @Entity @Table(name = "user_vm_view") @@ -398,6 +399,9 @@ public class UserVmJoinVO extends BaseViewWithTagInformationVO implements Contro @Column(name = "public_ip_address") private String publicIpAddress; + @Column(name = "dns_record_url") + private String dnsRecordUrl; + @Column(name = "user_data", updatable = true, nullable = true, length = 2048) private String userData; @@ -1089,4 +1093,8 @@ public void setLeaseExpiryAction(String leaseExpiryAction) { public String getLeaseActionExecution() { return leaseActionExecution; } + + public String getDnsRecordUrl() { + return dnsRecordUrl; + } } diff --git a/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java b/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java index 00863c28dd22..32a7a972129c 100644 --- a/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java +++ b/server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java @@ -311,7 +311,7 @@ public Pair, Integer> listFirewallRules(IListFirewa sb.and("id", sb.entity().getId(), Op.EQ); sb.and("trafficType", sb.entity().getTrafficType(), Op.EQ); - sb.and("networkId", sb.entity().getNetworkId(), Op.EQ); + sb.and("networkId", sb.entity().getNetworkId(), Op.EQ); sb.and("ip", sb.entity().getSourceIpAddressId(), Op.EQ); sb.and("purpose", sb.entity().getPurpose(), Op.EQ); sb.and("display", sb.entity().isDisplay(), Op.EQ); diff --git a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java index 0da403c35f2f..e12e66dc984d 100644 --- a/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java +++ b/server/src/main/java/com/cloud/resourcelimit/ResourceLimitManagerImpl.java @@ -322,6 +322,7 @@ public boolean configure(final String name, final Map params) th accountResourceLimitMap.put(Resource.ResourceType.backup_storage.name(), Long.parseLong(_configDao.getValue(BackupManager.DefaultMaxAccountBackupStorage.key()))); accountResourceLimitMap.put(Resource.ResourceType.bucket.name(), Long.parseLong(_configDao.getValue(BucketApiService.DefaultMaxAccountBuckets.key()))); accountResourceLimitMap.put(Resource.ResourceType.object_storage.name(), Long.parseLong(_configDao.getValue(BucketApiService.DefaultMaxAccountObjectStorage.key()))); + accountResourceLimitMap.put(ResourceType.dns_zone.name(), DefaultMaxDnsAccounts.value()); domainResourceLimitMap.put(Resource.ResourceType.public_ip.name(), Long.parseLong(_configDao.getValue(Config.DefaultMaxDomainPublicIPs.key()))); domainResourceLimitMap.put(Resource.ResourceType.snapshot.name(), Long.parseLong(_configDao.getValue(Config.DefaultMaxDomainSnapshots.key()))); diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 09ef9fe8bec9..2f6b64262b85 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -72,6 +72,7 @@ import org.apache.cloudstack.backup.BackupOffering; import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.DnsServer; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -178,9 +179,9 @@ import com.cloud.utils.ConstantTimeComparator; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; import com.cloud.utils.Ternary; import com.cloud.utils.UuidUtils; -import com.cloud.utils.StringUtils; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.Manager; import com.cloud.utils.component.ManagerBase; @@ -3634,6 +3635,20 @@ public void checkAccess(Account account, BackupOffering bof) throws PermissionDe throw new PermissionDeniedException("There's no way to confirm " + account + " has access to " + bof); } + @Override + public void checkAccess(Account caller, DnsServer dnsServer) throws PermissionDeniedException { + if (caller.getId() == dnsServer.getAccountId()) { + return; + } + if (!dnsServer.isPublicServer()) { + throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + dnsServer.getName()); + } + Account owner = getAccount(dnsServer.getAccountId()); + if (!_domainDao.isChildDomain(owner.getDomainId(), caller.getDomainId())) { + throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + dnsServer.getName()); + } + } + @Override public void checkAccess(User user, ControlledEntity entity) throws PermissionDeniedException { for (SecurityChecker checker : _securityCheckers) { diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java new file mode 100644 index 000000000000..9c8bfe4ba30d --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -0,0 +1,715 @@ +// 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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd; +import org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd; +import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsRecordCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsServerCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsZoneCmd; +import org.apache.cloudstack.api.command.user.dns.DisassociateDnsZoneFromNetworkCmd; +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.command.user.dns.UpdateDnsZoneCmd; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; +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.dao.DnsServerJoinDao; +import org.apache.cloudstack.dns.dao.DnsZoneDao; +import org.apache.cloudstack.dns.dao.DnsZoneJoinDao; +import org.apache.cloudstack.dns.dao.DnsZoneNetworkMapDao; +import org.apache.cloudstack.dns.exception.DnsNotFoundException; +import org.apache.cloudstack.dns.vo.DnsServerJoinVO; +import org.apache.cloudstack.dns.vo.DnsServerVO; +import org.apache.cloudstack.dns.vo.DnsZoneJoinVO; +import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; +import org.apache.cloudstack.dns.vo.DnsZoneVO; +import org.springframework.stereotype.Component; + +import com.cloud.domain.dao.DomainDao; +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.network.Network; +import com.cloud.network.dao.NetworkDao; +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; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.Nic; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.UserVmDao; + +@Component +public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderManager, PluggableService { + List dnsProviders; + @Inject + AccountManager accountMgr; + @Inject + DnsServerDao dnsServerDao; + @Inject + DnsZoneDao dnsZoneDao; + @Inject + NetworkDao networkDao; + @Inject + DnsZoneNetworkMapDao dnsZoneNetworkMapDao; + @Inject + UserVmDao userVmDao; + @Inject + NicDao nicDao; + @Inject + DomainDao domainDao; + @Inject + DnsZoneJoinDao dnsZoneJoinDao; + @Inject + DnsServerJoinDao dnsServerJoinDao; + + private DnsProvider getProviderByType(DnsProviderType type) { + if (type == null) { + throw new CloudRuntimeException("Provider type cannot be null"); + } + for (DnsProvider provider : dnsProviders) { + if (provider.getProviderType() == type) { + return provider; + } + } + throw new CloudRuntimeException("No plugin found for DNS provider type: " + type); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_SERVER_ADD, eventDescription = "Adding a DNS Server") + public DnsServer addDnsServer(AddDnsServerCmd cmd) { + 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()); + } + + boolean isDnsPublic = cmd.isPublic(); + String publicDomainSuffix = cmd.getPublicDomainSuffix(); + if (caller.getType().equals(Account.Type.NORMAL)) { + logger.info("Only admin and domain admin users are allowed to configure a public DNS server"); + isDnsPublic = false; + publicDomainSuffix = null; + } + + if (StringUtils.isNotBlank(publicDomainSuffix)) { + publicDomainSuffix = DnsProviderUtil.normalizeDomain(publicDomainSuffix); + } + + DnsProviderType type = cmd.getProvider(); + DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), cmd.getPort(), cmd.getExternalServerId(), type, + cmd.getDnsUserName(), cmd.getCredentials(), isDnsPublic, publicDomainSuffix, cmd.getNameServers(), + caller.getAccountId(), caller.getDomainId()); + try { + DnsProvider provider = getProviderByType(type); + String dnsServerId = provider.validateAndResolveServer(server); // returns localhost for PowerDNS + if (StringUtils.isNotBlank(dnsServerId)) { + server.setExternalServerId(dnsServerId); + } + return dnsServerDao.persist(server); + } catch (Exception ex) { + logger.error("Failed to validate DNS server", ex); + throw new CloudRuntimeException("Failed to validate DNS server"); + } + } + + @Override + public ListResponse listDnsServers(ListDnsServersCmd cmd) { + Pair, Integer> result = searchForDnsServerInternal(cmd); + ListResponse response = new ListResponse<>(); + if (result == null) { + return response; + } + List serverIds = new ArrayList<>(); + for (DnsServer server : result.first()) { + serverIds.add(server.getUuid()); + } + List joinResult = dnsServerJoinDao.listByUuids(serverIds); + List serverResponses = new ArrayList<>(); + for (DnsServerJoinVO server : joinResult) { + serverResponses.add(createDnsServerResponse(server)); + } + response.setResponses(serverResponses, result.second()); + return response; + } + + private Pair, Integer> searchForDnsServerInternal(ListDnsServersCmd cmd) { + Long dnsServerId = cmd.getId(); + Account caller = CallContext.current().getCallingAccount(); + if (dnsServerId != null) { + DnsServerVO dnsServerVO = dnsServerDao.findById(dnsServerId); + if (dnsServerVO == null) { + return null; + } + return new Pair<>(Collections.singletonList(dnsServerVO), 1); + } + Set parentDomainIds = domainDao.getDomainParentIds(caller.getDomainId()); + Filter searchFilter = new Filter(DnsServerVO.class, ApiConstants.ID, true, cmd.getStartIndex(), cmd.getPageSizeVal()); + return dnsServerDao.searchDnsServer(dnsServerId, caller.getAccountId(), parentDomainIds, cmd.getProviderType(), cmd.getKeyword(), searchFilter); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_SERVER_UPDATE, eventDescription = "Updating DNS Server") + 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(); + accountMgr.checkAccess(caller, dnsServer); + + 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(DnsProviderUtil.normalizeDomain(cmd.getPublicDomainSuffix())); + } + + if (cmd.getNameServers() != null) { + dnsServer.setNameServers(cmd.getNameServers()); + } + if (cmd.getState() != null) { + dnsServer.setState(cmd.getState()); + } + + if (validationRequired) { + DnsProvider provider = getProviderByType(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 + @ActionEvent(eventType = EventTypes.EVENT_DNS_SERVER_DELETE, eventDescription = "Deleting DNS Server") + public boolean deleteDnsServer(DeleteDnsServerCmd 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(); + accountMgr.checkAccess(caller, dnsServer); + return dnsServerDao.remove(dnsServerId); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_ZONE_DELETE, eventDescription = "Deleting DNS Zone") + public boolean deleteDnsZone(Long zoneId) { + DnsZoneVO zone = dnsZoneDao.findById(zoneId); + if (zone == null) { + throw new InvalidParameterValueException("DNS zone not found for the given ID."); + } + + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, true, zone); + DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); + if (server != null && zone.getState() == DnsZone.State.Active) { + try { + DnsProvider provider = getProviderByType(server.getProviderType()); + 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."); + } + } + return dnsZoneDao.remove(zoneId); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_ZONE_UPDATE, eventDescription = "Updating DNS Zone") + public DnsZone updateDnsZone(UpdateDnsZoneCmd cmd) { + DnsZoneVO dnsZone = dnsZoneDao.findById(cmd.getId()); + if (dnsZone == null) { + throw new InvalidParameterValueException("DNS zone not found."); + } + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, true, dnsZone); + boolean updated = false; + if (cmd.getDescription() != null) { + dnsZone.setDescription(cmd.getDescription()); + updated = true; + } + + if (updated) { + 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 = getProviderByType(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 dnsZone; + } + + @Override + public ListResponse listDnsZones(ListDnsZonesCmd cmd) { + Pair, Integer> result = searchForDnsZonesInternal(cmd); + List zoneIds = new ArrayList<>(); + for (DnsZoneVO zone : result.first()) { + zoneIds.add(zone.getUuid()); + } + List zoneJoinVos = dnsZoneJoinDao.listByUuids(zoneIds); + List zoneResponses = new ArrayList<>(); + for (DnsZoneJoinVO zoneJoin: zoneJoinVos) { + zoneResponses.add(createDnsZoneResponse(zoneJoin)); + } + ListResponse response = new ListResponse<>(); + response.setResponses(zoneResponses, result.second()); + return response; + } + + private Pair, Integer> searchForDnsZonesInternal(ListDnsZonesCmd cmd) { + if (cmd.getId() != null) { + DnsZone dnsZone = dnsZoneDao.findById(cmd.getId()); + if (dnsZone == null) { + throw new InvalidParameterValueException("DNS zone not found for the given ID"); + } + } + Account caller = CallContext.current().getCallingAccount(); + if (cmd.getDnsServerId() != null) { + DnsServer dnsServer = dnsServerDao.findById(cmd.getDnsServerId()); + accountMgr.checkAccess(caller, dnsServer); + } + List ownDnsServerIds = dnsServerDao.listDnsServerIdsByAccountId(caller.getAccountId()); + String keyword = cmd.getKeyword(); + if (cmd.getName() != null) { + keyword = cmd.getName(); + } + Filter searchFilter = new Filter(DnsZoneVO.class, ApiConstants.ID, true, cmd.getStartIndex(), cmd.getPageSizeVal()); + return dnsZoneDao.searchZones(cmd.getId(), caller.getAccountId(), ownDnsServerIds, cmd.getDnsServerId(), keyword, searchFilter); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_RECORD_CREATE, eventDescription = "Creating DNS Record") + public DnsRecordResponse createDnsRecord(CreateDnsRecordCmd cmd) { + String recordName = StringUtils.trimToEmpty(cmd.getName()).toLowerCase(); + if (StringUtils.isBlank(recordName)) { + throw new InvalidParameterValueException("Empty DNS record name is not allowed"); + } + DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); + if (zone == null) { + throw new InvalidParameterValueException("DNS zone not found."); + } + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, true, zone); + DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); + try { + DnsRecord.RecordType type = cmd.getType(); + List normalizedContents = cmd.getContents().stream() + .map(value -> DnsProviderUtil.normalizeDnsRecordValue(value, type)).collect(Collectors.toList()); + DnsRecord record = new DnsRecord(recordName, type, normalizedContents, cmd.getTtl()); + DnsProvider provider = getProviderByType(server.getProviderType()); + 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); + throw new CloudRuntimeException(String.format("Failed to add DNS record: %s", recordName)); + } + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_DNS_RECORD_DELETE, eventDescription = "Deleting DNS Record") + public boolean deleteDnsRecord(DeleteDnsRecordCmd cmd) { + DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); + if (zone == null) { + throw new InvalidParameterValueException("DNS zone not found."); + } + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, true, zone); + DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); + try { + DnsRecord record = new DnsRecord(); + record.setName(cmd.getName()); + record.setType(cmd.getType()); + DnsProvider provider = getProviderByType(server.getProviderType()); + return provider.deleteRecord(server, zone, record) != null; + } catch (Exception ex) { + logger.error("Failed to delete DNS record via provider", ex); + throw new CloudRuntimeException(String.format("Failed to delete DNS record: %s", cmd.getName())); + } + } + + @Override + public ListResponse listDnsRecords(ListDnsRecordsCmd cmd) { + DnsZoneVO zone = dnsZoneDao.findById(cmd.getDnsZoneId()); + if (zone == null) { + throw new InvalidParameterValueException("DNS zone not found for the given ID."); + } + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, true, zone); + DnsServerVO server = dnsServerDao.findById(zone.getDnsServerId()); + if (server == null) { + throw new CloudRuntimeException("The underlying DNS server for this DNS zone is missing."); + } + try { + DnsProvider provider = getProviderByType(server.getProviderType()); + List records = provider.listRecords(server, zone); + List responses = new ArrayList<>(); + for (DnsRecord record : records) { + responses.add(createDnsRecordResponse(record)); + } + + ListResponse listResponse = new ListResponse<>(); + listResponse.setResponses(responses, responses.size()); + return listResponse; + } catch (DnsNotFoundException ex) { + logger.error("DNS zone is not found", ex); + throw new CloudRuntimeException("DNS zone is not found, please register it first"); + } catch (Exception ex) { + logger.error("Failed to list DNS records from provider", ex); + throw new CloudRuntimeException("Failed to fetch DNS records"); + } + } + + @Override + public List listProviderNames() { + List providerNames = new ArrayList<>(); + if (dnsProviders != null) { + for (DnsProvider provider : dnsProviders) { + providerNames.add(provider.getProviderType().toString()); + } + } + return providerNames; + } + + @Override + public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) { + if (StringUtils.isBlank(cmd.getName())) { + throw new InvalidParameterValueException("DNS zone name cannot be empty"); + } + + String dnsZoneName = DnsProviderUtil.normalizeDomain(cmd.getName()); + DnsServerVO server = dnsServerDao.findById(cmd.getDnsServerId()); + if (server == null) { + throw new InvalidParameterValueException(String.format("DNS server not found for the given ID: %s", cmd.getDnsServerId())); + } + Account caller = CallContext.current().getCallingAccount(); + boolean isOwner = (server.getAccountId() == caller.getId()); + if (!isOwner) { + if (!server.isPublicServer()) { + throw new PermissionDeniedException("You do not have permission to use this DNS server."); + } + dnsZoneName = DnsProviderUtil.appendPublicSuffixToZone(dnsZoneName, server.getPublicDomainSuffix()); + } + DnsZone.ZoneType type = cmd.getType(); + DnsZoneVO existing = dnsZoneDao.findByNameServerAndType(dnsZoneName, server.getId(), type); + if (existing != null) { + throw new InvalidParameterValueException("DNS zone already exists on this server."); + } + DnsZoneVO dnsZoneVO = new DnsZoneVO(dnsZoneName, type, server.getId(), caller.getId(), caller.getDomainId(), cmd.getDescription()); + return dnsZoneDao.persist(dnsZoneVO); + } + + @Override + public DnsZone provisionDnsZone(long dnsZoneId) { + DnsZoneVO dnsZone = dnsZoneDao.findById(dnsZoneId); + if (dnsZone == null) { + throw new CloudRuntimeException("DNS zone not found during provisioning"); + } + DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId()); + try { + DnsProvider provider = getProviderByType(server.getProviderType()); + String externalReferenceId = provider.provisionZone(server, dnsZone); + dnsZone.setExternalReference(externalReferenceId); + dnsZone.setState(DnsZone.State.Active); + 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); + throw new CloudRuntimeException("Failed to provision DNS zone: " + dnsZone.getName()); + } + return dnsZone; + } + + + public DnsServerResponse createDnsServerResponse(DnsServer dnsServer) { + DnsServerJoinVO serverJoin = dnsServerJoinDao.findById(dnsServer.getId()); + return createDnsServerResponse(serverJoin); + } + + DnsServerResponse createDnsServerResponse(DnsServerJoinVO server) { + DnsServerResponse response = new DnsServerResponse(); + response.setId(server.getUuid()); + response.setName(server.getName()); + response.setUrl(server.getUrl()); + response.setPort(server.getPort()); + response.setProvider(server.getProviderType()); + response.setPublic(server.isPublicServer()); + response.setNameServers(server.getNameServers()); + response.setPublicDomainSuffix(server.getPublicDomainSuffix()); + response.setAccountName(server.getAccountName()); + response.setDomainId(server.getDomainUuid()); // Note: APIs always return UUIDs, not internal DB IDs! + response.setDomainName(server.getDomainName()); + response.setObjectName("dnsserver"); + return response; + } + + @Override + public DnsZoneResponse createDnsZoneResponse(DnsZone dnsZone) { + DnsZoneJoinVO zoneJoinVO = dnsZoneJoinDao.findById(dnsZone.getId()); + return createDnsZoneResponse(zoneJoinVO); + } + + DnsZoneResponse createDnsZoneResponse(DnsZoneJoinVO zone) { + DnsZoneResponse response = new DnsZoneResponse(); + response.setId(zone.getUuid()); + response.setName(zone.getName()); + response.setDnsServerId(zone.getDnsServerUuid()); + response.setAccountName(zone.getAccountName()); + response.setDomainId(zone.getDomainUuid()); + response.setDomainName(zone.getDomainName()); + response.setDnsServerName(zone.getDnsServerName()); + response.setDnsServerAccountName(zone.getDnsServerAccountName()); + response.setState(zone.getState()); + response.setDescription(zone.getDescription()); + return response; + } + + @Override + public DnsRecordResponse createDnsRecordResponse(DnsRecord record) { + DnsRecordResponse res = new DnsRecordResponse(); + res.setName(record.getName()); + res.setType(record.getType()); + res.setContent(record.getContents()); + return res; + } + + @Override + public DnsZoneNetworkMapResponse associateZoneToNetwork(AssociateDnsZoneToNetworkCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); + DnsZoneVO dnsZone = dnsZoneDao.findById(cmd.getDnsZoneId()); + if (dnsZone == null) { + throw new InvalidParameterValueException("DNS zone not found."); + } + accountMgr.checkAccess(caller, null, true, dnsZone); + + NetworkVO network = networkDao.findById(cmd.getNetworkId()); + if (network == null) { + throw new InvalidParameterValueException("Network not found."); + } + if (!NetworkVO.GuestType.Shared.equals(network.getGuestType())) { + throw new CloudRuntimeException(String.format("Operation is not permitted for network type: %s", network.getGuestType())); + } + accountMgr.checkAccess(caller, null, true, network); + + DnsZoneNetworkMapVO existing = dnsZoneNetworkMapDao.findByNetworkId(network.getId()); + if (existing != null) { + throw new InvalidParameterValueException("Network has existing DNS zone associated to it."); + } + DnsZoneNetworkMapVO mapping = new DnsZoneNetworkMapVO(dnsZone.getId(), network.getId(), cmd.getSubDomain()); + dnsZoneNetworkMapDao.persist(mapping); + DnsZoneNetworkMapResponse response = new DnsZoneNetworkMapResponse(); + response.setId(mapping.getUuid()); + response.setDnsZoneId(dnsZone.getUuid()); + response.setNetworkId(network.getUuid()); + response.setSubDomain(mapping.getSubDomain()); + return response; + } + + @Override + public boolean disassociateZoneFromNetwork(DisassociateDnsZoneFromNetworkCmd cmd) { + DnsZoneNetworkMapVO mapping = dnsZoneNetworkMapDao.findByNetworkId(cmd.getNetworkId()); + if (mapping == null) { + throw new InvalidParameterValueException("No DNS zone is associated to specified network."); + } + DnsZoneVO zone = dnsZoneDao.findById(mapping.getDnsZoneId()); + if (zone == null) { + // If the zone is missing but the mapping exists (shouldn't happen due to CASCADE DELETE), + // clean up the orphaned mapping. + return dnsZoneNetworkMapDao.remove(mapping.getId()); + } + Account caller = CallContext.current().getCallingAccount(); + accountMgr.checkAccess(caller, null, true, zone); + return dnsZoneNetworkMapDao.remove(mapping.getId()); + } + + @Override + public String processDnsRecordForInstance(VirtualMachine instance, Network network, Nic nic, boolean isAdd) { + long networkId = network.getId(); + DnsZoneNetworkMapVO dnsZoneNetworkMap = dnsZoneNetworkMapDao.findByNetworkId(networkId); + if (dnsZoneNetworkMap == null) { + logger.warn("No DNS zone is mapped to this network. Please associate a zone first."); + return null; + } + DnsZoneVO dnsZone = dnsZoneDao.findById(dnsZoneNetworkMap.getDnsZoneId()); + if (dnsZone == null || dnsZone.getState() != DnsZone.State.Active) { + return null; + } + DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId()); + // Construct FQDN Prefix (e.g., "instance-id" or "instance-id.subdomain") + String recordName = String.valueOf(instance.getInstanceName()); + if (StringUtils.isNotBlank(dnsZoneNetworkMap.getSubDomain())) { + recordName = recordName + "." + dnsZoneNetworkMap.getSubDomain(); + } + + try { + DnsProvider provider = getProviderByType(server.getProviderType()); + // Handle IPv4 (A Record) + String ipv4DnsRecord = null; + if (nic.getIPv4Address() != null) { + DnsRecord recordA = new DnsRecord(recordName, DnsRecord.RecordType.A, Collections.singletonList(nic.getIPv4Address()), 3600); + if (isAdd) { + ipv4DnsRecord = provider.addRecord(server, dnsZone, recordA); + } else { + ipv4DnsRecord = provider.deleteRecord(server, dnsZone, recordA); + } + } + + // Handle IPv6 (AAAA Record) if it exists + String ipv6DnsRecord = null; + if (nic.getIPv6Address() != null) { + DnsRecord recordAAAA = new DnsRecord(recordName, DnsRecord.RecordType.AAAA, Collections.singletonList(nic.getIPv6Address()), 3600); + if (isAdd) { + ipv6DnsRecord = provider.addRecord(server, dnsZone, recordAAAA); + } else { + ipv6DnsRecord = provider.deleteRecord(server, dnsZone, recordAAAA); + } + } + return ipv4DnsRecord != null ? ipv4DnsRecord : ipv6DnsRecord; + } catch (Exception ex) { + logger.error( + "Failed to {} DNS record for Instance {} in zone {}", + isAdd ? "register" : "remove", + instance.getHostName(), + dnsZone.getName(), + ex + ); + } + return null; + } + + @Override + public boolean start() { + if (dnsProviders == null || dnsProviders.isEmpty()) { + logger.warn("DNS Framework started but no provider plugins were found!"); + } else { + logger.info("DNS Framework started with: {} providers.", dnsProviders.size()); + } + return true; + } + + @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(UpdateDnsServerCmd.class); + + // DNS Zone Commands + cmdList.add(CreateDnsZoneCmd.class); + cmdList.add(ListDnsZonesCmd.class); + cmdList.add(DeleteDnsZoneCmd.class); + cmdList.add(UpdateDnsZoneCmd.class); + cmdList.add(AssociateDnsZoneToNetworkCmd.class); + cmdList.add(DisassociateDnsZoneFromNetworkCmd.class); + + // DNS Record Commands + cmdList.add(CreateDnsRecordCmd.class); + cmdList.add(ListDnsRecordsCmd.class); + cmdList.add(DeleteDnsRecordCmd.class); + return cmdList; + } + + public List getDnsProviders() { + return dnsProviders; + } + + public void setDnsProviders(List dnsProviders) { + this.dnsProviders = dnsProviders; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderUtil.java b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderUtil.java new file mode 100644 index 000000000000..95f04fe09408 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderUtil.java @@ -0,0 +1,102 @@ +// 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; + +import org.apache.commons.validator.routines.DomainValidator; + +import com.cloud.utils.StringUtils; + +public class DnsProviderUtil { + static DomainValidator validator = DomainValidator.getInstance(true); + + public static String appendPublicSuffixToZone(String zoneName, String suffixDomain) { + if (StringUtils.isBlank(suffixDomain)) { + return zoneName; + } + suffixDomain = DnsProviderUtil.normalizeDomain(suffixDomain); + // Already suffixed → return as-is + if (zoneName.toLowerCase().endsWith("." + suffixDomain.toLowerCase())) { + return zoneName; + } + + if (zoneName.equals(suffixDomain)) { + throw new IllegalArgumentException("Cannot create DNS zone at root-level: " + suffixDomain); + } + // Check TLD matches + String tldUser = getTld(zoneName); + String tldSuffix = getTld(suffixDomain); + + if (!tldUser.equalsIgnoreCase(tldSuffix)) { + throw new IllegalArgumentException("TLD mismatch between user zone and domain suffix"); + } + // Remove TLD from userZone + int lastDot = zoneName.lastIndexOf('.'); + String zonePrefix = zoneName.substring(0, lastDot); + return zonePrefix + "." + suffixDomain; + } + + private static String getTld(String domain) { + String[] labels = domain.split("\\."); + return labels[labels.length - 1]; + } + + public static String normalizeDomain(String domain) { + if (StringUtils.isBlank(domain)) { + throw new IllegalArgumentException("Domain cannot be empty"); + } + + String normalized = domain.trim().toLowerCase(); + if (normalized.endsWith(".")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + // Validate domain, allow local/private TLDs + if (!validator.isValid(normalized)) { + throw new IllegalArgumentException("Invalid domain name: " + domain); + } + return normalized; + } + + public static String normalizeDnsRecordValue(String value, DnsRecord.RecordType recordType) { + if (StringUtils.isBlank(value)) { + throw new IllegalArgumentException("DNS record value cannot be empty"); + } + switch (recordType) { + case A: + case AAAA: + // IP addresses: trim only + return value.trim(); + + case CNAME: + case NS: + case PTR: + case SRV: + // Domain names: normalize like zones + return normalizeDomain(value); + case MX: + // PowerDNS MX: contains priority + domain, only trim and lowercase + return value.trim().toLowerCase(); + + case TXT: + // Free text: preserve exactly + return value; + + default: + throw new IllegalArgumentException("Unsupported DNS record type: " + recordType); + } + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java b/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java new file mode 100644 index 000000000000..53f6fb62105f --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java @@ -0,0 +1,201 @@ +// +// 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; + +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.framework.events.Event; +import org.apache.cloudstack.framework.events.EventBus; +import org.apache.cloudstack.framework.events.EventBusException; +import org.apache.cloudstack.framework.events.EventSubscriber; +import org.apache.cloudstack.framework.events.EventTopic; +import org.springframework.stereotype.Component; + +import com.cloud.event.EventTypes; +import com.cloud.network.Network; +import com.cloud.network.dao.NetworkDao; +import com.cloud.utils.StringUtils; +import com.cloud.utils.component.ManagerBase; +import com.cloud.vm.Nic; +import com.cloud.vm.NicVO; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicDetailsDao; +import com.cloud.vm.dao.VMInstanceDao; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Component +public class DnsVmLifecycleListener extends ManagerBase implements EventSubscriber { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Inject + private EventBus eventBus = null; + + @Inject + VMInstanceDao vmInstanceDao; + @Inject + NetworkDao networkDao; + @Inject + NicDao nicDao; + @Inject + DnsProviderManager providerManager; + @Inject + NicDetailsDao nicDetailsDao; + + @Override + public boolean configure(final String name, final Map params) { + if (eventBus == null) { + logger.info("EventBus is not available; DNS Instance lifecycle listener will not subscribe to events"); + return true; + } + try { + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_CREATE, null, null, null), this); + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_START, null, null, null), this); + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_STOP, null, null, null), this); + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_DESTROY, null, null, null), this); + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_CREATE, null, null, null), this); + eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_DELETE, null, null, null), this); + } catch (EventBusException ex) { + logger.error("Failed to subscribe DnsVmLifecycleListener to EventBus", ex); + } + return true; + } + + @Override + public void onEvent(Event event) { + JsonNode descJson = parseEventDescription(event); + if (!isEventCompleted(descJson)) { + return; + } + + String eventType = event.getEventType(); + String resourceUuid = event.getResourceUUID(); + try { + switch (eventType) { + case EventTypes.EVENT_VM_CREATE: + case EventTypes.EVENT_VM_START: + handleVmEvent(resourceUuid, true); + break; + case EventTypes.EVENT_VM_STOP: + case EventTypes.EVENT_VM_DESTROY: + handleVmEvent(resourceUuid, false); + break; + case EventTypes.EVENT_NIC_CREATE: + handleNicEvent(descJson, true); + break; + case EventTypes.EVENT_NIC_DELETE: + handleNicEvent(descJson, false); + break; + default: + break; + } + } catch (Exception ex) { + logger.error("Failed to process DNS lifecycle event: type={}, resourceUuid={}", + eventType, event.getResourceUUID(), ex); + } + } + + private void handleNicEvent(JsonNode eventDesc, boolean isAddDnsRecord) { + JsonNode nicUuid = eventDesc.get("Nic"); + JsonNode vmUuid = eventDesc.get("VirtualMachine"); + if (nicUuid == null || nicUuid.isNull() || vmUuid == null || vmUuid.isNull()) { + logger.warn("Event has missing data to work on: {}", eventDesc); + return; + } + VMInstanceVO vmInstanceVO = vmInstanceDao.findByUuid(vmUuid.asText()); + if (vmInstanceVO == null) { + logger.error("Unable to find Instance with ID: {}", vmUuid); + return; + } + Nic nic = nicDao.findByUuidIncludingRemoved(nicUuid.asText()); + if (nic == null) { + logger.error("NIC is not found for the ID: {}", nicUuid); + return; + } + Network network = networkDao.findById(nic.getNetworkId()); + if (network == null || !Network.GuestType.Shared.equals(network.getGuestType())) { + logger.warn("Network is not eligible for DNS record registration"); + return; + } + processEventForDnsRecord(vmInstanceVO, network, nic, isAddDnsRecord); + } + + private void handleVmEvent(String vmUuid, boolean isAddDnsRecord) { + VMInstanceVO vmInstanceVO = vmInstanceDao.findByUuid(vmUuid); + if (vmInstanceVO == null) { + logger.error("Unable to find Instance with ID: {}", vmUuid); + return; + } + List vmNics = nicDao.listByVmId(vmInstanceVO.getId()); + for (NicVO nic : vmNics) { + Network network = networkDao.findById(nic.getNetworkId()); + if (network == null || !Network.GuestType.Shared.equals(network.getGuestType())) { + continue; + } + processEventForDnsRecord(vmInstanceVO, network, nic, isAddDnsRecord); + } + } + + void processEventForDnsRecord(VMInstanceVO vmInstanceVO, Network network, Nic nic, boolean isAddDnsRecord) { + String dnsRecordUrl = providerManager.processDnsRecordForInstance(vmInstanceVO, network, nic, isAddDnsRecord); + if (dnsRecordUrl != null) { + if (isAddDnsRecord) { + nicDetailsDao.addDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD, dnsRecordUrl, true); + } else { + nicDetailsDao.removeDetail(nic.getId(), ApiConstants.NIC_DNS_RECORD); + } + } else { + logger.error("Failure {} DNS record for Instance: {} for Network with ID: {}", + isAddDnsRecord ? "adding" : "removing", vmInstanceVO.getUuid(), network.getUuid()); + } + } + + private JsonNode parseEventDescription(Event event) { + String rawDescription = event.getDescription(); + if (StringUtils.isBlank(rawDescription)) { + return null; + } + try { + return OBJECT_MAPPER.readTree(rawDescription); + } catch (Exception ex) { + logger.warn("parseEventDescription: failed to parse description for event [{}]: {}", + event.getEventType(), ex.getMessage()); + return null; + } + } + + private boolean isEventCompleted(JsonNode descJson) { + if (descJson == null) { + return false; + } + JsonNode statusNode = descJson.get(ApiConstants.STATUS); + if (statusNode == null || statusNode.isNull()) { + return false; + } + + logger.debug("Processing Event: {}", descJson); + return ApiConstants.COMPLETED.equalsIgnoreCase(statusNode.asText()); + } +} 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 new file mode 100644 index 000000000000..8a884620c4b6 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDao.java @@ -0,0 +1,39 @@ +// 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.dao; + +import java.util.List; +import java.util.Set; + +import org.apache.cloudstack.dns.DnsProviderType; +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 { + DnsServer findByUrlAndAccount(String url, long accountId); + + List listDnsServerIdsByAccountId(Long accountId); + + Pair, Integer> searchDnsServers(Long id, String keyword, String provider, Long accountId, Filter filter); + + Pair, Integer> searchDnsServer(Long dnsServerId, Long accountId, Set domainIds, DnsProviderType providerType, String keyword, 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 new file mode 100644 index 000000000000..095b4eb640c2 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerDaoImpl.java @@ -0,0 +1,142 @@ +// 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.dao; + +import java.util.List; +import java.util.Set; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.vo.DnsServerVO; +import org.apache.commons.collections.CollectionUtils; +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.GenericSearchBuilder; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class DnsServerDaoImpl extends GenericDaoBase implements DnsServerDao { + SearchBuilder AllFieldsSearch; + SearchBuilder AccountUrlSearch; + GenericSearchBuilder DnsServerIdsByAccountSearch; + + + public DnsServerDaoImpl() { + super(); + + 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(); + + DnsServerIdsByAccountSearch = createSearchBuilder(Long.class); + DnsServerIdsByAccountSearch.selectFields(DnsServerIdsByAccountSearch.entity().getId()); + DnsServerIdsByAccountSearch.and(ApiConstants.ACCOUNT_ID, DnsServerIdsByAccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + DnsServerIdsByAccountSearch.and(ApiConstants.STATE, DnsServerIdsByAccountSearch.entity().getState(), SearchCriteria.Op.EQ); + DnsServerIdsByAccountSearch.done(); + + } + + @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 List listDnsServerIdsByAccountId(Long accountId) { + SearchCriteria sc = DnsServerIdsByAccountSearch.create(); + if (accountId != null) { + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + } + sc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled); + return customSearch(sc, null); + } + + @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); + } + + @Override + public Pair, Integer> searchDnsServer(Long dnsServerId, Long accountId, Set domainIds, DnsProviderType providerType, + String keyword, Filter filter) { + + SearchBuilder sb = createSearchBuilder(); + sb.and(ApiConstants.ID, sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.NAME, sb.entity().getName(), SearchCriteria.Op.LIKE); + + sb.and().op(ApiConstants.ACCOUNT_ID, sb.entity().getAccountId(), SearchCriteria.Op.EQ); + if (!CollectionUtils.isEmpty(domainIds)) { + sb.or().op(ApiConstants.IS_PUBLIC, sb.entity().isPublicServer(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.DOMAIN_IDS, sb.entity().getDomainId(), SearchCriteria.Op.IN); + sb.cp(); + } + sb.cp(); + sb.and(ApiConstants.PROVIDER_TYPE, sb.entity().getProviderType(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.STATE, sb.entity().getState(), SearchCriteria.Op.EQ); + sb.done(); + + SearchCriteria sc = sb.create(); + if (dnsServerId != null) { + sc.setParameters(ApiConstants.ID, dnsServerId); + } + if (accountId != null) { + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + } + if (!CollectionUtils.isEmpty(domainIds)) { + sc.setParameters(ApiConstants.IS_PUBLIC, true); + sc.setParameters(ApiConstants.DOMAIN_IDS, domainIds.toArray()); + } + if (providerType != null) { + sc.setParameters(ApiConstants.PROVIDER_TYPE, providerType); + } + if (keyword != null) { + sc.setParameters(ApiConstants.NAME, "%" + keyword + "%"); + } + sc.setParameters(ApiConstants.STATE, DnsServer.State.Enabled); + return searchAndCount(sc, filter); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDao.java new file mode 100644 index 000000000000..b1932d6709cb --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDao.java @@ -0,0 +1,26 @@ +// 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.dao; + +import org.apache.cloudstack.dns.vo.DnsServerJoinVO; + +import com.cloud.utils.db.GenericDao; + +public interface DnsServerJoinDao extends GenericDao { + +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDaoImpl.java new file mode 100644 index 000000000000..28a2e9d6638a --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsServerJoinDaoImpl.java @@ -0,0 +1,25 @@ +// 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.dao; + +import org.apache.cloudstack.dns.vo.DnsServerJoinVO; + +import com.cloud.utils.db.GenericDaoBase; + +public class DnsServerJoinDaoImpl extends GenericDaoBase implements DnsServerJoinDao { +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java new file mode 100644 index 000000000000..05fb799a663a --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDao.java @@ -0,0 +1,35 @@ +// 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.dao; + +import java.util.List; + +import org.apache.cloudstack.dns.DnsZone; +import org.apache.cloudstack.dns.vo.DnsZoneVO; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDao; + +public interface DnsZoneDao extends GenericDao { + List listByAccount(long accountId); + DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type); + + Pair, Integer> searchZones(Long id, Long accountId, List ownDnsServerIds, Long targetDnsServerId, + String keyword, Filter filter); +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java new file mode 100644 index 000000000000..1aad6fa0f0be --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImpl.java @@ -0,0 +1,110 @@ +// 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.dao; + +import java.util.List; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.DnsZone; +import org.apache.cloudstack.dns.vo.DnsZoneVO; +import org.apache.commons.collections.CollectionUtils; +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 DnsZoneDaoImpl extends GenericDaoBase implements DnsZoneDao { + SearchBuilder AccountSearch; + SearchBuilder NameServerTypeSearch; + + public DnsZoneDaoImpl() { + super(); + + AccountSearch = createSearchBuilder(); + AccountSearch.and(ApiConstants.ACCOUNT_ID, AccountSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AccountSearch.and(ApiConstants.STATE, AccountSearch.entity().getState(), SearchCriteria.Op.EQ); + AccountSearch.done(); + + NameServerTypeSearch = createSearchBuilder(); + NameServerTypeSearch.and(ApiConstants.NAME, NameServerTypeSearch.entity().getName(), SearchCriteria.Op.EQ); + NameServerTypeSearch.and(ApiConstants.DNS_SERVER_ID, NameServerTypeSearch.entity().getDnsServerId(), SearchCriteria.Op.EQ); + NameServerTypeSearch.and(ApiConstants.TYPE, NameServerTypeSearch.entity().getType(), SearchCriteria.Op.EQ); + NameServerTypeSearch.and(ApiConstants.STATE, NameServerTypeSearch.entity().getState(), SearchCriteria.Op.EQ); + NameServerTypeSearch.done(); + } + + @Override + public List listByAccount(long accountId) { + SearchCriteria sc = AccountSearch.create(); + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + sc.setParameters(ApiConstants.STATE, DnsZone.State.Active); + return listBy(sc); + } + + @Override + public DnsZoneVO findByNameServerAndType(String name, long dnsServerId, DnsZone.ZoneType type) { + SearchCriteria sc = NameServerTypeSearch.create(); + sc.setParameters(ApiConstants.NAME, name); + sc.setParameters(ApiConstants.DNS_SERVER_ID, dnsServerId); + sc.setParameters(ApiConstants.TYPE, type); + sc.setParameters(ApiConstants.STATE, DnsZone.State.Active); + return findOneBy(sc); + } + + @Override + public Pair, Integer> searchZones(Long id, Long accountId, List ownDnsServerIds, Long targetDnsServerId, + String keyword, Filter filter) { + + SearchBuilder sb = createSearchBuilder(); + sb.and(ApiConstants.STATE, sb.entity().getState(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.ID, sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and(ApiConstants.NAME, sb.entity().getName(), SearchCriteria.Op.LIKE); + sb.and(ApiConstants.TARGET_ID, sb.entity().getDnsServerId(), SearchCriteria.Op.EQ); + if (!CollectionUtils.isEmpty(ownDnsServerIds)) { + sb.and().op(ApiConstants.DNS_SERVER_ID, sb.entity().getDnsServerId(), SearchCriteria.Op.IN); + sb.or(ApiConstants.ACCOUNT_ID, sb.entity().getAccountId(), SearchCriteria.Op.EQ); + sb.cp(); + } else { + sb.and(ApiConstants.ACCOUNT_ID, sb.entity().getAccountId(), SearchCriteria.Op.EQ); + } + sb.done(); + + SearchCriteria sc = sb.create(); + if (id != null) { + sc.setParameters(ApiConstants.ID, id); + } + if (!CollectionUtils.isEmpty(ownDnsServerIds)) { + sc.setParameters(ApiConstants.DNS_SERVER_ID, ownDnsServerIds.toArray()); + } + if (keyword != null) { + sc.setParameters(ApiConstants.NAME, "%" + keyword + "%"); + } + if (accountId != null) { + sc.setParameters(ApiConstants.ACCOUNT_ID, accountId); + } + if (targetDnsServerId != null) { + sc.setParameters(ApiConstants.TARGET_ID, targetDnsServerId); + } + sc.setParameters(ApiConstants.STATE, DnsZone.State.Active); + return searchAndCount(sc, filter); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDao.java new file mode 100644 index 000000000000..ee0bcc5bcae2 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDao.java @@ -0,0 +1,26 @@ +// 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.dao; + +import org.apache.cloudstack.dns.vo.DnsZoneJoinVO; + +import com.cloud.utils.db.GenericDao; + +public interface DnsZoneJoinDao extends GenericDao { + +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDaoImpl.java new file mode 100644 index 000000000000..6be2a3a798e3 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneJoinDaoImpl.java @@ -0,0 +1,25 @@ +// 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.dao; + +import org.apache.cloudstack.dns.vo.DnsZoneJoinVO; + +import com.cloud.utils.db.GenericDaoBase; + +public class DnsZoneJoinDaoImpl extends GenericDaoBase implements DnsZoneJoinDao { +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java new file mode 100644 index 000000000000..29e9190d542c --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDao.java @@ -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.dao; + +import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; + +import com.cloud.utils.db.GenericDao; + +public interface DnsZoneNetworkMapDao extends GenericDao { + DnsZoneNetworkMapVO findByZoneAndNetwork(long dnsZoneId, long networkId); + DnsZoneNetworkMapVO findByNetworkId(long networkId); +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java new file mode 100644 index 000000000000..01a8718a8955 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/dao/DnsZoneNetworkMapDaoImpl.java @@ -0,0 +1,60 @@ +// 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.dao; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; +import org.springframework.stereotype.Component; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@Component +public class DnsZoneNetworkMapDaoImpl extends GenericDaoBase implements DnsZoneNetworkMapDao { + private final SearchBuilder ZoneNetworkSearch; + private final SearchBuilder NetworkSearch; + + public DnsZoneNetworkMapDaoImpl() { + super(); + ZoneNetworkSearch = createSearchBuilder(); + ZoneNetworkSearch.and(ApiConstants.DNS_ZONE_ID, ZoneNetworkSearch.entity().getDnsZoneId(), SearchCriteria.Op.EQ); + ZoneNetworkSearch.and(ApiConstants.NETWORK_ID, ZoneNetworkSearch.entity().getNetworkId(), SearchCriteria.Op.EQ); + ZoneNetworkSearch.done(); + + NetworkSearch = createSearchBuilder(); + NetworkSearch.and(ApiConstants.NETWORK_ID, NetworkSearch.entity().getNetworkId(), SearchCriteria.Op.EQ); + NetworkSearch.done(); + } + + @Override + public DnsZoneNetworkMapVO findByZoneAndNetwork(long dnsZoneId, long networkId) { + SearchCriteria sc = ZoneNetworkSearch.create(); + sc.setParameters(ApiConstants.DNS_ZONE_ID, dnsZoneId); + sc.setParameters(ApiConstants.NETWORK_ID, networkId); + + return findOneBy(sc); + } + + @Override + public DnsZoneNetworkMapVO findByNetworkId(long networkId) { + SearchCriteria sc = NetworkSearch.create(); + sc.setParameters(ApiConstants.NETWORK_ID, networkId); + return findOneBy(sc); + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerJoinVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerJoinVO.java new file mode 100644 index 000000000000..ae59efc51c21 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerJoinVO.java @@ -0,0 +1,154 @@ +/* + * 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.vo; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.dns.DnsServer; + +import com.cloud.api.query.vo.BaseViewVO; +import com.cloud.utils.StringUtils; + +@Entity +@Table(name = "dns_server_view") +public class DnsServerJoinVO extends BaseViewVO implements InternalIdentity, Identity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "provider_type") + private String providerType; + + @Column(name = "url") + private String url; + + @Column(name = "port") + private Integer port; + + @Column(name = "name_servers") + private String nameServers; + + @Column(name = "is_public") + private boolean isPublic; + + @Column(name = "public_domain_suffix") + private String publicDomainSuffix; + + @Column(name = "state") + @Enumerated(value = EnumType.STRING) + private DnsServer.State state; + + @Column(name = "account_name") + private String accountName; + + @Column(name = "domain_name") + private String domainName; + + @Column(name = "domain_uuid") + private String domainUuid; + + @Column(name = "domain_path") + private String domainPath; + + public DnsServerJoinVO() { + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public String getProviderType() { + return providerType; + } + + public String getUrl() { + return url; + } + + public Integer getPort() { + return port; + } + + public List getNameServers() { + if (StringUtils.isBlank(nameServers)) { + return Collections.emptyList(); + } + return Arrays.asList(nameServers.split(",")); + } + + public boolean isPublicServer() { + return isPublic; + } + + public String getPublicDomainSuffix() { + return publicDomainSuffix; + } + + public DnsServer.State getState() { + return state; + } + + public String getAccountName() { + return accountName; + } + + public String getDomainName() { + return domainName; + } + + public String getDomainPath() { + return domainPath; + } + + public String getDomainUuid() { + return domainUuid; + } +} 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 new file mode 100644 index 000000000000..5ccc5bf91f9e --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsServerVO.java @@ -0,0 +1,256 @@ +// 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.vo; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +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; + +@Entity +@Table(name = "dns_server") +public class DnsServerVO implements DnsServer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "url") + private String url; + + @Column(name = "port") + private Integer port; + + @Column(name = "provider_type") + @Enumerated(EnumType.STRING) + private DnsProviderType providerType; + + @Column(name = "dns_username") + private String dnsUserName; + + @Encrypt + @Column(name = "api_key") + private String apiKey; + + @Column(name = "external_server_id") + private String externalServerId; + + @Column(name = "is_public") + private boolean publicServer; + + @Column(name = "public_domain_suffix") + private String publicDomainSuffix; + + @Column(name = "state") + @Enumerated(EnumType.STRING) + private State state; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "domain_id") + private long domainId; + + @Column(name = "name_servers") + private String nameServers; + + @Column(name = GenericDao.CREATED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed = null; + + DnsServerVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + } + + public DnsServerVO(String name, String url, Integer port, String externalServerId, DnsProviderType providerType, String dnsUserName, String apiKey, + boolean isPublic, String publicDomainSuffix, List nameServers, Long accountId, Long domainId) { + this(); + this.name = name; + this.url = url; + this.port = port; + this.externalServerId = externalServerId; + this.providerType = providerType; + this.dnsUserName = dnsUserName; + this.apiKey = apiKey; + this.accountId = accountId; + this.domainId = domainId; + this.publicDomainSuffix = publicDomainSuffix; + this.publicServer = isPublic; + this.state = State.Enabled; + this.nameServers = String.join(",", nameServers);; + } + + @Override + public long getId() { + return id; + } + + @Override + public Class getEntityType() { + return DnsZone.class; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getUrl() { + return url; + } + + @Override + public DnsProviderType getProviderType() { + return providerType; + } + + @Override + public String getApiKey() { + return apiKey; + } + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public Date getCreated() { + return created; + } + + @Override + public Date getRemoved() { + return removed; + } + + @Override + public String getUuid() { + return uuid; + } + + public boolean isPublicServer() { + return publicServer; + } + + public State getState() { + return state; + } + + 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) { + publicServer = 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; + } + + public List getNameServers() { + if (StringUtils.isBlank(nameServers)) { + return Collections.emptyList(); + } + return Arrays.asList(nameServers.split(",")); + } + + @Override + public long getDomainId() { + return domainId; + } + + public String getPublicDomainSuffix() { + return publicDomainSuffix; + } + + public String getExternalServerId() { + return externalServerId; + } + + public void setExternalServerId(String externalServerId) { + this.externalServerId = externalServerId; + } + + public Integer getPort() { + return this.port; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneJoinVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneJoinVO.java new file mode 100644 index 000000000000..4468a4f64e0e --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneJoinVO.java @@ -0,0 +1,130 @@ +/* + * 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.vo; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.api.query.vo.BaseViewVO; + +@Entity +@Table(name = "dns_zone_view") +public class DnsZoneJoinVO extends BaseViewVO implements InternalIdentity, Identity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "state") + @Enumerated(value = EnumType.STRING) + private DnsZone.State state; + + @Column(name = "dns_server_uuid") + private String dnsServerUuid; + + @Column(name = "dns_server_name") + private String dnsServerName; + + @Column(name = "dns_server_account_name") + private String dnsServerAccountName; + + @Column(name = "account_name") + private String accountName; + + @Column(name = "domain_name") + private String domainName; + + @Column(name = "domain_uuid") + private String domainUuid; + + @Column(name = "domain_path") + private String domainPath; + + @Column(name = "description") + private String description; + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public DnsZone.State getState() { + return state; + } + + public String getDnsServerUuid() { + return dnsServerUuid; + } + + public String getDnsServerName() { + return dnsServerName; + } + + public String getAccountName() { + return accountName; + } + + public String getDomainName() { + return domainName; + } + + public String getDomainPath() { + return domainPath; + } + + public String getName() { + return name; + } + + public String getDnsServerAccountName() { + return dnsServerAccountName; + } + + public String getDomainUuid() { + return domainUuid; + } + + public String getDescription() { + return description; + } + +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java new file mode 100644 index 000000000000..a94e12549209 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneNetworkMapVO.java @@ -0,0 +1,105 @@ +// 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.vo; + +import java.util.Date; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.api.InternalIdentity; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "dns_zone_network_map") +public class DnsZoneNetworkMapVO implements InternalIdentity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "dns_zone_id") + private long dnsZoneId; + + @Column(name = "network_id") + private long networkId; + + @Column(name = "sub_domain") + private String subDomain; + + @Column(name = GenericDao.CREATED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date created = null; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed = null; + + public DnsZoneNetworkMapVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + } + + public DnsZoneNetworkMapVO(long dnsZoneId, long networkId, String subDomain) { + this(); + this.dnsZoneId = dnsZoneId; + this.networkId = networkId; + this.subDomain = subDomain; + } + + @Override + public long getId() { + return id; + } + + public long getDnsZoneId() { + return dnsZoneId; + } + + public long getNetworkId() { + return networkId; + } + + public String getSubDomain() { + return subDomain; + } + + public Date getCreated() { + return created; + } + + public Date getRemoved() { + return removed; + } + + public String getUuid() { + return uuid; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java new file mode 100644 index 000000000000..ca0817208b92 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/dns/vo/DnsZoneVO.java @@ -0,0 +1,164 @@ +// 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.vo; + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.dns.DnsZone; + +import com.cloud.utils.db.GenericDao; + +@Entity +@Table(name = "dns_zone") +public class DnsZoneVO implements DnsZone { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "name") + private String name; + + @Column(name = "dns_server_id") + private long dnsServerId; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "domain_id") + private long domainId; + + @Column(name = "description") + private String description; + + @Column(name = "external_reference") + private String externalReference; + + @Column(name = "type") + @Enumerated(EnumType.STRING) + private ZoneType type; + + @Column(name = "state") + @Enumerated(EnumType.STRING) + private State state; + + @Column(name = GenericDao.CREATED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date created = null; + + @Column(name = GenericDao.REMOVED_COLUMN) + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed = null; + + public DnsZoneVO() { + this.uuid = UUID.randomUUID().toString(); + this.created = new Date(); + this.state = State.Inactive; + } + + public DnsZoneVO(String name, ZoneType type, long dnsServerId, long accountId, long domainId, String description) { + this(); + this.name = name; + this.type = (type != null) ? type : ZoneType.Public; + this.dnsServerId = dnsServerId; + this.accountId = accountId; + this.domainId = domainId; + this.description = description; + } + + @Override + public Class getEntityType() { + return DnsZone.class; + } + + @Override + public String getName() { + return name; + } + + @Override + public long getDnsServerId() { + return dnsServerId; + } + + @Override + public long getAccountId() { + return accountId; + } + + @Override + public ZoneType getType() { + return type; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public List getAssociatedNetworks() { + return List.of(); + } + + @Override + public State getState() { + return state; + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public long getId() { + return id; + } + + @Override + public long getDomainId() { + return domainId; + } + + public void setState(State state) { this.state = state; } + + public void setDescription(String description) { + this.description = description; + } + + public void setExternalReference(String externalReference) { + this.externalReference = externalReference; + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml new file mode 100644 index 000000000000..6751e8a34b26 --- /dev/null +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-event-bus-context.xml @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index b90c40dc95e7..f49573892590 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -398,4 +398,9 @@ + + + + + diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index e41a04ff2e1b..ee7624250f6f 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -273,7 +273,8 @@ 'Extensions' : 'Extension', 'CustomAction' : 'Extension', 'CustomActions' : 'Extension', - 'ImportVmTask': 'Import VM Task' + 'ImportVmTask': 'Import VM Task', + 'Dns': 'DNS' } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 8bcc5d0a94bf..e82eb80e7d97 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -91,6 +91,7 @@ "label.action.delete.account": "Delete Account", "label.action.delete.backup.offering": "Delete backup offering", "label.action.delete.cluster": "Delete Cluster", +"label.action.delete.dns.server": "Delete DNS Server", "label.action.delete.domain": "Delete Domain", "label.action.delete.egress.firewall": "Delete Egress Firewall Rule", "label.action.delete.firewall": "Delete Firewall Rule", @@ -291,6 +292,11 @@ "label.add.internal.lb": "Add internal LB", "label.add.ip.range": "Add IP Range", "label.add.ipv4.subnet": "Add IPv4 Subnet for Routed Networks", +"label.dns.server": "DNS Server", +"label.dns.add.server": "Add DNS Server", +"label.dns.update.server": "Update DNS Server", +"label.dns.delete.server": "Delete DNS Server", +"label.dnsrecords": "DNS Records", "label.add.ip.v6.prefix": "Add IPv6 prefix", "label.add.isolated.network": "Add Isolated Network", "label.add.kubernetes.cluster": "Add Kubernetes Cluster", @@ -712,6 +718,8 @@ "label.created": "Created", "label.creating": "Creating", "label.creating.iprange": "Creating IP ranges", +"label.dns.credentials": "DNS API key", +"label.nameservers": "DNS Nameservers", "label.credit": "Credit", "label.cron": "Cron expression", "label.cron.mode": "Cron mode", @@ -935,6 +943,9 @@ "label.dns": "DNS", "label.dns1": "DNS 1", "label.dns2": "DNS 2", +"label.dns.records": "DNS Records", +"label.dns.zone": "DNS Zone", +"label.dns.zones": "DNS Zones", "label.domain": "Domain", "label.domain.id": "Domain ID", "label.domain.name": "Domain name", @@ -2943,6 +2954,7 @@ "message.action.delete.backup.schedule": "Please confirm that you want to delete this backup schedule?", "message.action.delete.cluster": "Please confirm that you want to delete this Cluster.", "message.action.delete.custom.action": "Please confirm that you want to delete this custom action.", +"message.action.delete.dns.server": "Please confirm you want to delete this DNS server.", "message.action.delete.domain": "Please confirm that you want to delete this domain.", "message.action.delete.extension": "Please confirm that you want to delete the extension", "message.action.delete.external.firewall": "Please confirm that you would like to remove this external firewall. Warning: If you are planning to add back the same external firewall, you must reset usage data on the device.", diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 4145eeb9be6d..37b06809b61c 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -207,6 +207,13 @@ + +
+ {{ $t('label.provider') }} +
+
{{ dataResource[item] }}
+
+
diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index 33b39d271726..62f5ae199554 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -1487,6 +1487,83 @@ export default { groupMap: (selection) => { return selection.map(x => { return { id: x } }) } } ] + }, + { + name: 'dnsrecords', + title: 'label.dns.records', + icon: 'global-outlined', + hidden: true, + permission: ['listDnsRecords'], + columns: ['name', 'url', 'provider'], + details: ['name', 'url', 'provider', 'ispublic', 'port', 'nameservers'], + related: [{ + name: 'vm', + title: 'label.dns.zone', + param: 'dnszoneid' + }] + }, + { + name: 'dnszones', + title: 'label.dns.zones', + icon: 'global-outlined', + hidden: true, + permission: ['listDnsZones'], + columns: ['name', 'state', 'dnsservername', 'dnsserveraccount'], + details: ['name', 'state', 'dnsservername', 'dnsserveraccount'], + tabs: [{ + name: 'details', + component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue'))) + }, + { + name: 'dnsrecords', + component: shallowRef(defineAsyncComponent(() => import('@/views/network/InternalLBAssignedVmTab.vue'))), + show: () => true + }] + }, + { + name: 'dnsserver', + title: 'label.dns.server', + icon: 'global-outlined', + permission: ['listDnsServers'], + columns: ['name', 'url', 'provider'], + details: ['name', 'url', 'abc', 'provider', 'ispublic', 'port', 'nameservers', 'domain', 'account'], + related: [{ + name: 'dnszones', + title: 'label.dns.zone', + param: 'dnsserverid' + }], + actions: [ + { + api: 'addDnsServer', + icon: 'plus-outlined', + label: 'label.dns.add.server', + listView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/network/dns/AddDnsServer.vue'))), + show: () => { + return true + } + }, + { + api: 'updateDnsServer', + icon: 'edit-outlined', + label: 'label.dns.update.server', + dataView: true, + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/network/dns/AddDnsServer.vue'))), + show: (record) => { return true } + }, + { + api: 'deleteDnsServer', + icon: 'delete-outlined', + label: 'label.dns.delete.server', + message: 'message.action.delete.dns.server', + dataView: true, + groupAction: true, + show: (record) => { return true }, + groupMap: (selection) => { return selection.map(x => { return { id: x } }) } + } + ] } ] } diff --git a/ui/src/views/network/NicsTable.vue b/ui/src/views/network/NicsTable.vue index a31925a6b374..e9e15a0aa90c 100644 --- a/ui/src/views/network/NicsTable.vue +++ b/ui/src/views/network/NicsTable.vue @@ -57,6 +57,10 @@ {{ record.isolationuri }} + + + {{ record.dns_url }} + diff --git a/ui/src/views/network/dns/AddDnsServer.vue b/ui/src/views/network/dns/AddDnsServer.vue new file mode 100644 index 000000000000..404870f9d829 --- /dev/null +++ b/ui/src/views/network/dns/AddDnsServer.vue @@ -0,0 +1,243 @@ +// 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. + + + + + +