From 9d0884a0a6c5d8afe80a7299a239f3b32e530cd8 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 15 Apr 2026 18:57:46 +0530 Subject: [PATCH] add marvin test for powerdns --- .../apache/cloudstack/api/ApiConstants.java | 1 - .../api/command/user/dns/AddDnsServerCmd.java | 8 +- .../command/user/dns/UpdateDnsServerCmd.java | 8 +- .../command/user/dns/AddDnsServerCmdTest.java | 4 +- .../user/dns/UpdateDnsServerCmdTest.java | 4 +- .../META-INF/db/schema-42210to42300.sql | 6 +- .../dns/DnsProviderManagerImpl.java | 6 +- .../dns/DnsProviderManagerImplTest.java | 2 +- .../smoke/test_dns_framework_powerdns.py | 318 ++++++++++++++++++ 9 files changed, 335 insertions(+), 22 deletions(-) create mode 100644 test/integration/smoke/test_dns_framework_powerdns.py 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 b664f7c3bb2..fe50c50b198 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1351,7 +1351,6 @@ public class ApiConstants { // 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_ZONE = "dnszone"; public static final String DNS_RECORD = "dnsrecord"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java index 40464096181..8ae9af8f61e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmd.java @@ -62,8 +62,8 @@ public class AddDnsServerCmd extends BaseCmd { 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.API_KEY, required = true, type = CommandType.STRING, description = "API key or token for the external provider") + private String apiKey; @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") private Integer port; @@ -89,8 +89,8 @@ public class AddDnsServerCmd extends BaseCmd { public String getUrl() { return url; } - public String getCredentials() { - return credentials; + public String getApiKey() { + return apiKey; } public Integer getPort() { 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 index 52ba0497e74..cb992381d77 100644 --- 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 @@ -58,8 +58,8 @@ public class UpdateDnsServerCmd extends BaseCmd { @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.API_KEY, type = CommandType.STRING, required = false, description = "API Key or Credentials for the external provider") + private String apiKey; @Parameter(name = ApiConstants.PORT, type = CommandType.INTEGER, description = "Port number of the external DNS server") private Integer port; @@ -83,8 +83,8 @@ public class UpdateDnsServerCmd extends BaseCmd { public Long getId() { return id; } public String getName() { return name; } public String getUrl() { return url; } - public String getCredentials() { - return credentials; + public String getApiKey() { + return apiKey; } public Integer getPort() { return port; diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmdTest.java index 1d03e1749c2..31df626bcd4 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/AddDnsServerCmdTest.java @@ -40,7 +40,7 @@ public class AddDnsServerCmdTest extends BaseDnsCmdTest { setField(cmd, "name", "test-dns"); setField(cmd, "url", "http://dns.example.com"); setField(cmd, "provider", "PowerDNS"); - setField(cmd, "credentials", "api-key-123"); + setField(cmd, "apiKey", "api-key-123"); setField(cmd, "port", 8081); setField(cmd, "isPublic", true); setField(cmd, "publicDomainSuffix", "public.example.com"); @@ -56,7 +56,7 @@ public class AddDnsServerCmdTest extends BaseDnsCmdTest { assertEquals("test-dns", cmd.getName()); assertEquals("http://dns.example.com", cmd.getUrl()); - assertEquals("api-key-123", cmd.getCredentials()); + assertEquals("api-key-123", cmd.getApiKey()); assertEquals(Integer.valueOf(8081), cmd.getPort()); assertTrue(cmd.isPublic()); assertEquals("public.example.com", cmd.getPublicDomainSuffix()); diff --git a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmdTest.java index 02ef4043ece..96ddf265452 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/user/dns/UpdateDnsServerCmdTest.java @@ -39,7 +39,7 @@ public class UpdateDnsServerCmdTest extends BaseDnsCmdTest { setField(cmd, "id", ENTITY_ID); setField(cmd, "name", "updated-dns"); setField(cmd, "url", "http://updated.dns.com"); - setField(cmd, "credentials", "new-api-key"); + setField(cmd, "apiKey", "new-api-key"); setField(cmd, "port", 9090); setField(cmd, "isPublic", true); setField(cmd, "publicDomainSuffix", "updated.example.com"); @@ -55,7 +55,7 @@ public class UpdateDnsServerCmdTest extends BaseDnsCmdTest { assertEquals(Long.valueOf(ENTITY_ID), cmd.getId()); assertEquals("updated-dns", cmd.getName()); assertEquals("http://updated.dns.com", cmd.getUrl()); - assertEquals("new-api-key", cmd.getCredentials()); + assertEquals("new-api-key", cmd.getApiKey()); assertEquals(Integer.valueOf(9090), cmd.getPort()); assertTrue(cmd.isPublic()); assertEquals("updated.example.com", cmd.getPublicDomainSuffix()); 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 c891a2ca875..8768ed33213 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 @@ -131,7 +131,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`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', + `api_key` varchar(255) NOT NULL COMMENT 'api key or token for the dns server ', `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', @@ -186,7 +186,3 @@ CREATE TABLE IF NOT EXISTS `cloud`.`dns_zone_network_map` ( 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/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java index d9da2303065..d8aecca7e03 100644 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java @@ -177,7 +177,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa 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(), + cmd.getDnsUserName(), cmd.getApiKey(), isDnsPublic, publicDomainSuffix, cmd.getNameServers(), caller.getAccountId(), caller.getDomainId()); try { DnsProvider provider = getProviderByType(type); @@ -250,8 +250,8 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa } } - if (cmd.getCredentials() != null && !cmd.getCredentials().equals(originalKey)) { - dnsServer.setApiKey(cmd.getCredentials()); + if (cmd.getApiKey() != null && !cmd.getApiKey().equals(originalKey)) { + dnsServer.setApiKey(cmd.getApiKey()); validationRequired = true; } diff --git a/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java index fdfff0a9e83..61e2c61340b 100644 --- a/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java @@ -849,7 +849,7 @@ public class DnsProviderManagerImplTest { org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd cmd = mock( org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd.class); when(cmd.getId()).thenReturn(SERVER_ID); - when(cmd.getCredentials()).thenReturn("new-api-key"); + when(cmd.getApiKey()).thenReturn("new-api-key"); when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); Mockito.doReturn("old-api-key").when(serverVO).getApiKey(); diff --git a/test/integration/smoke/test_dns_framework_powerdns.py b/test/integration/smoke/test_dns_framework_powerdns.py new file mode 100644 index 00000000000..8c29e0ea168 --- /dev/null +++ b/test/integration/smoke/test_dns_framework_powerdns.py @@ -0,0 +1,318 @@ +# 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. + +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.cloudstackAPI import * + +import subprocess +import time +import logging +import socket + +class TestCloudStackDNSFramework(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + """ + Pre-requisite: + Bring up PDNS via docker compose (external dependency for DNS provider). + """ + super(TestCloudStackDNSFramework, cls).setUpClass() + cls.api_client = cls.testClient.getApiClient() + + cls.logger = logging.getLogger("TestCloudStackDNSFramework") + cls.stream_handler = logging.StreamHandler() + cls.logger.setLevel(logging.DEBUG) + cls.logger.addHandler(cls.stream_handler) + # ------------------------- + # Detect Marvin VM IP (reachable by MS) + # ------------------------- + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("8.8.8.8", 80)) + cls.marvin_vm_ip = s.getsockname()[0] + finally: + s.close() + + cls.logger.info(f"Detected Marvin VM IP: {cls.marvin_vm_ip}") + + # ------------------------- + # PDNS compose config + # ------------------------- + + cls.compose_file = "/marvin/pdns/docker-compose.yml" + cls.compose_dir = "/marvin/pdns" + cls.logger.info("Bringing up PDNS via docker compose...") + + up_cmd = [ + "docker", "compose", + "-f", cls.compose_file, + "up", "-d" + ] + + result = subprocess.run( + up_cmd, + cwd=cls.compose_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + if result.returncode != 0: + cls.tearDownClass() + raise Exception(f"Failed to start PDNS:\n{result.stderr}") + + # Allow PDNS to initialize + time.sleep(15) + + cls.logger.info("PDNS is up and running") + + # Construct PDNS URL once + cls.pdns_url = f"http://{cls.marvin_vm_ip}" + cls.logger.info(f"PDNS endpoint: {cls.pdns_url}") + + + def test_01_list_dns_providers(self): + """ + List DNS providers, expect PowerDNS provider to be present + """ + list_providers_cmd = listDnsProviders.listDnsProvidersCmd() + self.logger.info("Listing DNS providers to verify PowerDNS presence") + response = self.api_client.listDnsProviders(list_providers_cmd) + self.assertIsNotNone(response, "Failed to list DNS providers") + self.logger.info(f"DNS Providers found: {[provider.name for provider in response]}") + + def test_02_add_dns_server(self): + """ + Register PDNS as DNS provider in CloudStack + """ + self.logger.info("Adding PDNS DNS server") + + response = self._add_dns_server() + self.assertIsNotNone(response, "Failed to add DNS provider") + self.__class__.dns_server_id = response.id + self.logger.info(f"DNS Provider added: {response.id}") + self.assertIsNotNone(response.id, "DNS server ID should not be None") + + + def test_03_list_dns_servers(self): + """ + List DNS servers and verify the newly added PDNS provider is present + """ + self.logger.info("Listing DNS servers to verify addition") + list_cmd = listDnsServers.listDnsServersCmd() + list_cmd.id = self.dns_server_id + response = self.api_client.listDnsServers(list_cmd) + self.assertIsNotNone(response, "Failed to list DNS servers") + self.assertEqual(len(response), 1, "Expected exactly one DNS server") + self.assertEqual(response[0].id, self.dns_server_id, "DNS server ID mismatch") + + + def test_04_create_dns_zone(self): + """ + Create a DNS zone in the added PDNS provider + """ + self.logger.info("Creating a DNS zone") + response = self._create_zone(self.dns_server_id) + self.assertIsNotNone(response, "Failed to create DNS zone") + self.assertIsNotNone(response.id, "DNS zone ID should not be None") + self.__class__.dns_zone_id = response.id + self.logger.info(f"DNS Zone created: {response.id}") + + + def test_05_list_dns_zones(self): + """ + List DNS zones and verify the newly created zone is present + """ + self.logger.info("Listing DNS zones to verify creation") + list_zones_cmd = listDnsZones.listDnsZonesCmd() + list_zones_cmd.id = self.dns_zone_id + response = self.api_client.listDnsZones(list_zones_cmd) + self.assertIsNotNone(response, "Failed to list DNS zones") + self.assertEqual(len(response), 1, "Expected exactly one DNS zone") + self.assertEqual(response[0].id, self.dns_zone_id, "DNS zone ID mismatch") + self.assertEqual(response[0].name, "example.com", "DNS zone name mismatch") + + def test_06_create_a_dns_record(self): + """ + Create a DNS record in the previously created zone + """ + self.logger.info("Creating A DNS record") + response = self._create_record( + self.dns_zone_id, + "www.example.com", + "A", + "10.1.1.10" + ) + self.assertIsNotNone(response, "Failed to create DNS record") + self.assertEqual(response.name, "www.example.com", "DNS record name mismatch") + + def test_07_create_aaaa_dns_records(self): + """ + Create AAAA DNS records in the previously created zone + """ + self.logger.info("Creating AAAA DNS records") + response = self._create_record( + self.dns_zone_id, + "www.example.com", + "AAAA", + "2001:db8::10" + ) + self.assertIsNotNone(response, "Failed to create AAAA DNS record") + self.assertTrue(response.name is not None, "DNS record name should not be None") + + + def test_08_create_mx_dns_record(self): + """ + Create an MX DNS record in the previously created zone + """ + self.logger.info("Creating an MX DNS record") + response = self._create_record( + self.dns_zone_id, + "example.com", + "MX", + "10 mail.example.com" + ) + self.assertIsNotNone(response, "Failed to create MX DNS record") + self.assertTrue(response.name is not None, "DNS record name should not be None") + + + def test_09_list_dns_records(self): + """ + List DNS records in the zone and verify the created records are present + """ + self.logger.info("Listing DNS records to verify creation") + list_records_cmd = listDnsRecords.listDnsRecordsCmd() + list_records_cmd.dnszoneid = self.dns_zone_id + response = self.api_client.listDnsRecords(list_records_cmd) + self.assertIsNotNone(response, "Failed to list DNS records") + self.assertEqual(len(response), 4, "Expected four DNS records, including NS record") + record_types = set(record.type for record in response) + self.assertSetEqual(record_types, {"NS", "A", "AAAA", "MX"}, "DNS record types mismatch") + + + def test_10_delete_dns_record(self): + """ + Delete one of the DNS records and verify it's removed + """ + self.logger.info("Deleting a DNS record") + delete_record_cmd = deleteDnsRecord.deleteDnsRecordCmd() + delete_record_cmd.name = "www.example.com" + delete_record_cmd.type = "A" + delete_record_cmd.dnszoneid = self.dns_zone_id + delete_response = self.api_client.deleteDnsRecord(delete_record_cmd) + self.assertIsNotNone(delete_response, "Failed to delete DNS record") + self.logger.info(f"DNS Record deleted: {delete_record_cmd.name}") + + # Verify deletion + list_record_cmd = listDnsRecords.listDnsRecordsCmd() + list_record_cmd.dnszoneid = self.dns_zone_id + response_after_deletion = self.api_client.listDnsRecords(list_record_cmd) + self.assertEqual(len(response_after_deletion), 3, "Expected three DNS records after deletion") + remaining_record_names = set(record.name for record in response_after_deletion) + self.assertNotIn(delete_record_cmd.name, remaining_record_names, "Deleted DNS record still present") + + def test_11_delete_dns_zone(self): + """ + Delete the DNS zone and verify it's removed + """ + self.logger.info("Deleting the DNS zone") + delete_zone_cmd = deleteDnsZone.deleteDnsZoneCmd() + delete_zone_cmd.id = self.dns_zone_id + response = self.api_client.deleteDnsZone(delete_zone_cmd) + self.assertIsNotNone(response, "Failed to delete DNS zone") + self.logger.info(f"DNS Zone deleted: {self.dns_zone_id}") + + # Verify deletion + list_zones_cmd = listDnsZones.listDnsZonesCmd() + list_zones_cmd.id = self.dns_zone_id + try: + self.api_client.listDnsZones(list_zones_cmd) + self.fail("DNS zone still exists after deletion") + except Exception as e: + self.logger.info(f"Expected exception after delete: {str(e)}") + + def test_12_delete_dns_server(self): + """ + Delete the PDNS DNS server and verify it's removed + """ + self.logger.info("Deleting the PDNS DNS server") + delete_cmd = deleteDnsServer.deleteDnsServerCmd() + delete_cmd.id = self.dns_server_id + response = self.api_client.deleteDnsServer(delete_cmd) + self.assertIsNotNone(response, "Failed to delete DNS server") + self.logger.info(f"DNS Server deleted: {self.dns_server_id}") + + # Verify deletion + list_cmd = listDnsServers.listDnsServersCmd() + list_cmd.id = self.dns_server_id + response = self.api_client.listDnsServers(list_cmd) + dns_servers = response or [] + self.assertEqual(len(dns_servers), 0, "Expected no DNS servers after deletion") + + @classmethod + def tearDownClass(cls): + """ + Stop PDNS after tests + """ + + try: + cls.logger.info("Stopping PDNS stack...") + + cmd = [ + "docker", "compose", + "-f", cls.compose_file, + "down" + ] + + subprocess.run(cmd, cwd=cls.compose_dir) + + finally: + super(TestCloudStackDNSFramework, cls).tearDownClass() + + + def _create_record(self, zone_id, name, rtype, contents): + cmd = createDnsRecord.createDnsRecordCmd() + cmd.dnszoneid = zone_id + cmd.name = name + cmd.type = rtype + cmd.contents = contents + + return self.api_client.createDnsRecord(cmd) + + + def _add_dns_server(self): + cmd = addDnsServer.addDnsServerCmd() + cmd.name = "pdns-server" + cmd.url = self.pdns_url + cmd.credentials = "supersecretapikey" + cmd.provider = "PowerDNS" + cmd.nameservers = ["ns1.example.com", "ns2.example.com"] + cmd.externalserverid = "localhost" + cmd.ispublic = True + cmd.port = 8081 + cmd.publicdomainsuffix = "pdns-public.example.com" + + return self.api_client.addDnsServer(cmd) + + def _create_zone(self, server_id): + cmd = createDnsZone.createDnsZoneCmd() + cmd.dnsserverid = server_id + cmd.name = "example.com" + cmd.description = "Test DNS Zone for PDNS" + + return self.api_client.createDnsZone(cmd) \ No newline at end of file